@abide/abide 0.123.2 → 0.124.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 (150) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +58 -0
  3. package/bin/abide.ts +36 -65
  4. package/docs/REGISTRY.md +45 -12
  5. package/docs/RULEBOOK.md +24 -14
  6. package/docs/abide.md +999 -459
  7. package/package.json +1 -1
  8. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  9. package/src/compiler/Diagnostics.ts +198 -0
  10. package/src/compiler/KeptChecker.ts +97 -0
  11. package/src/compiler/LoweredFile.ts +609 -0
  12. package/src/compiler/Lowering.ts +18 -1
  13. package/src/compiler/PositionedRefusal.ts +1 -1
  14. package/src/compiler/SHARED_NAMES.ts +2 -0
  15. package/src/compiler/ShadowHost.ts +670 -0
  16. package/src/compiler/ShadowMap.ts +1 -1
  17. package/src/compiler/TypeSession.ts +76 -1743
  18. package/src/compiler/appLayout.ts +28 -20
  19. package/src/compiler/appLints.ts +109 -0
  20. package/src/compiler/argsDefaultsIn.ts +1 -1
  21. package/src/compiler/checkApp.ts +64 -68
  22. package/src/compiler/checkedShadow.ts +2 -2
  23. package/src/compiler/classifyRead.ts +16 -1
  24. package/src/compiler/compileApp.ts +102 -71
  25. package/src/compiler/declaredExports.ts +46 -21
  26. package/src/compiler/diagnoseSources.ts +232 -0
  27. package/src/compiler/emitClient.ts +1 -1
  28. package/src/compiler/formatAbide.ts +7 -8
  29. package/src/compiler/handlersUnder.ts +32 -63
  30. package/src/compiler/lineOf.ts +0 -19
  31. package/src/compiler/matchersOf.ts +3 -3
  32. package/src/compiler/mountsOf.ts +3 -3
  33. package/src/compiler/reactiveReads.ts +235 -0
  34. package/src/compiler/reactiveTypes.ts +13 -0
  35. package/src/compiler/readSources.ts +16 -18
  36. package/src/compiler/readsIn.ts +181 -82
  37. package/src/compiler/routesDeclaration.ts +46 -0
  38. package/src/compiler/scaffold.ts +7 -2
  39. package/src/compiler/serveLanguage.ts +46 -74
  40. package/src/compiler/sharedModuleWrites.ts +1 -1
  41. package/src/compiler/splitImports.ts +2 -2
  42. package/src/compiler/templateWarnings.ts +1 -2
  43. package/src/compiler/trackingWarnings.ts +1 -1
  44. package/src/server/CLIENT_BUILD.ts +4 -32
  45. package/src/server/DesktopSession.ts +213 -0
  46. package/src/server/Followers.ts +399 -0
  47. package/src/server/MODULE_QUERIES.ts +6 -2
  48. package/src/server/PageRender.ts +201 -0
  49. package/src/server/RESUME_MILLISECONDS.ts +2 -0
  50. package/src/server/abideModules.ts +7 -7
  51. package/src/server/answerFailure.ts +3 -2
  52. package/src/server/answerRequest.ts +16 -369
  53. package/src/server/answerRpc.ts +48 -302
  54. package/src/server/answerSocket.ts +41 -490
  55. package/src/server/appBuild.ts +377 -0
  56. package/src/server/appKit.ts +1 -1
  57. package/src/server/appPlugins.ts +15 -17
  58. package/src/server/appTree.ts +6 -5
  59. package/src/server/argsContract.ts +209 -0
  60. package/src/server/bridgeRelay.ts +153 -65
  61. package/src/server/claudeCode.ts +30 -127
  62. package/src/server/config.ts +6 -7
  63. package/src/server/createApp.ts +38 -66
  64. package/src/server/csp.ts +1 -1
  65. package/src/server/deferredBuilds.ts +3 -16
  66. package/src/server/devWorker.ts +11 -15
  67. package/src/server/faceOf.ts +1 -1
  68. package/src/server/generatedSurfaces.ts +2 -1
  69. package/src/server/handlerFileMount.ts +40 -0
  70. package/src/server/index.ts +3 -3
  71. package/src/server/installServer.ts +5 -9
  72. package/src/server/invokeHandler.ts +230 -0
  73. package/src/server/mountHandlers.ts +184 -0
  74. package/src/server/openDesktopWindow.ts +308 -0
  75. package/src/server/packageDirectory.ts +14 -0
  76. package/src/server/preload.ts +21 -2
  77. package/src/server/principal.ts +5 -4
  78. package/src/server/remote.ts +2 -2
  79. package/src/server/render.ts +31 -59
  80. package/src/server/requestScope.ts +5 -36
  81. package/src/server/runCompiled.ts +1 -1
  82. package/src/server/runDesktop.ts +20 -448
  83. package/src/server/seedBuffer.ts +12 -12
  84. package/src/server/serveDev.ts +24 -58
  85. package/src/server/servedApp.ts +390 -0
  86. package/src/server/shell.ts +1 -1
  87. package/src/server/signInChanged.ts +20 -0
  88. package/src/server/surfaceOf.ts +3 -2
  89. package/src/server/versionOf.ts +7 -16
  90. package/src/shared/RPC_SERVER.ts +27 -31
  91. package/src/shared/Schema.ts +1 -2
  92. package/src/shared/applyPatch.ts +1 -1
  93. package/src/shared/index.ts +1 -1
  94. package/src/shared/{note.ts → issueReport.ts} +22 -0
  95. package/src/shared/loadCall.ts +66 -0
  96. package/src/shared/muxFrames.ts +1 -1
  97. package/src/shared/reactive/Keyed.ts +24 -16
  98. package/src/shared/reactive/Reactive.ts +37 -991
  99. package/src/shared/reactive/argsContract.ts +131 -4
  100. package/src/shared/reactive/byOf.ts +2 -5
  101. package/src/shared/reactive/callLoad.ts +8 -14
  102. package/src/shared/reactive/cellShape.ts +734 -0
  103. package/src/shared/reactive/index.ts +12 -23
  104. package/src/shared/reactive/pairReactive.ts +5 -3
  105. package/src/shared/reactive/patchLog.ts +79 -0
  106. package/src/shared/reactive/patchMarks.ts +34 -0
  107. package/src/shared/reactive/producer.ts +121 -0
  108. package/src/shared/reactive/recordWrites.ts +11 -14
  109. package/src/shared/reactive/scoped.ts +3 -2
  110. package/src/shared/reactive/selections.ts +209 -0
  111. package/src/shared/reactive/share.ts +4 -3
  112. package/src/shared/reactive/watch.ts +3 -4
  113. package/src/shared/rpc.ts +20 -24
  114. package/src/shared/validateJson.ts +1 -2
  115. package/src/shared/writePath.ts +29 -0
  116. package/src/ui/ArmRegion.ts +10 -4
  117. package/src/ui/Level.ts +11 -20
  118. package/src/ui/LevelChain.ts +272 -0
  119. package/src/ui/Router.ts +106 -313
  120. package/src/ui/Scope.ts +17 -3
  121. package/src/ui/TryRegion.ts +22 -23
  122. package/src/ui/bindSpread.ts +2 -4
  123. package/src/ui/listenForRoutes.ts +3 -10
  124. package/src/ui/outboxQueue.ts +23 -11
  125. package/src/ui/outcomeOf.ts +120 -0
  126. package/src/ui/reconcile.ts +4 -6
  127. package/src/ui/routing.ts +2 -1
  128. package/src/ui/rows.ts +2 -2
  129. package/src/ui/setAttribute.ts +12 -3
  130. package/src/ui/socketWire.ts +28 -13
  131. package/src/ui/wire.ts +9 -10
  132. package/src/compiler/Finding.ts +0 -2
  133. package/src/compiler/Refusals.ts +0 -65
  134. package/src/compiler/formatDiagnosis.ts +0 -20
  135. package/src/server/appDirectories.ts +0 -9
  136. package/src/server/argsFor.ts +0 -39
  137. package/src/server/buildDirectory.ts +0 -10
  138. package/src/server/coerceArgs.ts +0 -109
  139. package/src/server/derivedArgs.ts +0 -26
  140. package/src/server/runBuild.ts +0 -24
  141. package/src/server/serveBuild.ts +0 -144
  142. package/src/server/testBuild.ts +0 -220
  143. package/src/shared/LOAD_OFFLINE.ts +0 -15
  144. package/src/shared/LOAD_SIGNAL.ts +0 -10
  145. package/src/shared/issuesOf.ts +0 -18
  146. package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
  147. package/src/shared/reactive/FOLLOWED.ts +0 -8
  148. package/src/shared/reactive/PATCHES.ts +0 -11
  149. package/src/shared/reactive/PATCH_SINKS.ts +0 -26
  150. package/src/shared/reactive/canonicalKey.ts +0 -95
@@ -16,16 +16,15 @@ import {
16
16
  } from '#shared/routePattern.ts'
17
17
 
18
18
  import { abideImportsOf, componentGraph } from './componentGraph.ts'
19
+ import { type Diagnosis, Diagnostics } from './Diagnostics.ts'
19
20
  import type { HandlerFile } from './handlersUnder.ts'
20
21
  import { hookCalls, hooksIn } from './hookCalls.ts'
21
22
  import { matchersOf, type MountedTree } from './matchersOf.ts'
22
23
  import { mountsOf } from './mountsOf.ts'
23
24
  import { type AbideFile, scriptBodies } from './parseAbide.ts'
24
25
  import { type ParsedSource, parseSources } from './readSources.ts'
25
- import { Refusals } from './Refusals.ts'
26
26
  import { resolverIn } from './resolverIn.ts'
27
27
  import { attributedImports } from './splitImports.ts'
28
- import type { Diagnosis } from './TypeSession.ts'
29
28
 
30
29
  // THE APP, LAID OUT ONCE PER COMPILE OR CHECK: every `.abide` and handler file it holds — its own, a
31
30
  // dependency's it imports, a package's it mounts — each with where it sits and what that makes it
@@ -110,15 +109,15 @@ export type AppLayout = {
110
109
  */
111
110
  export async function appLayout(root: string): Promise<AppLayout> {
112
111
  // COLLECTED, whoever asks: a layout is kept across checks (`AppChecker`), and a build adopts
113
- // what it found (`Refusals.adopt`).
114
- const refusals = new Refusals(root, true)
112
+ // what it found (`Diagnostics.adopt`).
113
+ const refusals = new Diagnostics(root, true)
115
114
  const { abide, handlers, params, modules, published } = appFiles(
116
115
  root,
117
116
  refusals,
118
117
  )
119
118
  await refuseModuleHooks(root, modules, refusals)
120
119
  const resolve = resolverIn()
121
- const unparsed = new Refusals(root, true)
120
+ const unparsed = new Diagnostics(root, true)
122
121
  const own = parseSources(
123
122
  await textsAt(abide.map((path) => `${root}/${path}`)),
124
123
  unparsed,
@@ -140,13 +139,7 @@ export async function appLayout(root: string): Promise<AppLayout> {
140
139
  found.positions.get(source.path) ?? relative,
141
140
  )
142
141
  refusePlace(source, placed, resolve, refusals)
143
- sources.push({
144
- ...source,
145
- ...placed,
146
- paints: paints(source.file),
147
- invoked: invoked.has(source.path),
148
- hooks: hooksIn(scriptBodies(source.file)),
149
- })
142
+ sources.push(placedSource(source, placed, invoked.has(source.path)))
150
143
  }
151
144
  const texts = await textsAt(found.handlers.map((file) => file.path))
152
145
  const placedHandlers: PlacedHandler[] = []
@@ -209,8 +202,8 @@ export async function appLayout(root: string): Promise<AppLayout> {
209
202
  errors,
210
203
  published,
211
204
  addresses: [...addresses].sort(),
212
- refused: refusals.found,
213
- unparsed: unparsed.found,
205
+ refused: refusals.refused,
206
+ unparsed: unparsed.refused,
214
207
  resolve,
215
208
  }
216
209
  }
@@ -250,9 +243,9 @@ export async function withDependencies(
250
243
  parsed: readonly ParsedSource[],
251
244
  own: readonly string[],
252
245
  resolve: (specifier: string, from: string) => string | undefined,
253
- refusals: Refusals,
246
+ refusals: Diagnostics,
254
247
  /** Where what the parser refuses of a file reached here goes, where apart from `refusals`. */
255
- unparsed: Refusals = refusals,
248
+ unparsed: Diagnostics = refusals,
256
249
  ): Promise<{
257
250
  sources: ParsedSource[]
258
251
  handlers: HandlerFile[]
@@ -347,6 +340,21 @@ export async function withDependencies(
347
340
  }
348
341
 
349
342
  /** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
343
+ /** `source` at `placed`, with what its own text says it paints and calls. */
344
+ export function placedSource(
345
+ source: ParsedSource,
346
+ placed: Placement,
347
+ invoked: boolean,
348
+ ): PlacedSource {
349
+ return {
350
+ ...source,
351
+ ...placed,
352
+ paints: paints(source.file),
353
+ invoked,
354
+ hooks: hooksIn(scriptBodies(source.file)),
355
+ }
356
+ }
357
+
350
358
  export function paints(file: AbideFile): boolean {
351
359
  return file.template.length > 0 || file.head.length > 0
352
360
  }
@@ -362,7 +370,7 @@ function refusePlace(
362
370
  source: ParsedSource,
363
371
  placed: Placement,
364
372
  resolve: (specifier: string, from: string) => string | undefined,
365
- refusals: Refusals,
373
+ refusals: Diagnostics,
366
374
  ): void {
367
375
  const { relative, place, kind } = placed
368
376
  if (source.file.servers.length > 0 && place === 'public')
@@ -404,7 +412,7 @@ function refusePlace(
404
412
  async function refuseModuleHooks(
405
413
  root: string,
406
414
  modules: readonly string[],
407
- refusals: Refusals,
415
+ refusals: Diagnostics,
408
416
  ): Promise<void> {
409
417
  const texts = await textsAt(modules.map((module) => `${root}/${module}`))
410
418
  for (let at = 0; at < modules.length; at += 1) {
@@ -428,7 +436,7 @@ async function refuseModuleHooks(
428
436
  function refuseStrayLauncher(
429
437
  sources: readonly PlacedSource[],
430
438
  handlers: readonly PlacedHandler[],
431
- refusals: Refusals,
439
+ refusals: Diagnostics,
432
440
  ): void {
433
441
  const stray = (relative: string): void =>
434
442
  refusals.at(
@@ -457,7 +465,7 @@ function refuseStrayLauncher(
457
465
  */
458
466
  function routesOf(
459
467
  sources: readonly PlacedSource[],
460
- refusals: Refusals,
468
+ refusals: Diagnostics,
461
469
  ): { routes: AppRoute[]; errors: AppRoute[] } {
462
470
  const layouts: { source: string; segments: Segment[] }[] = []
463
471
  const layoutAt = new Map<string, string>()
@@ -0,0 +1,109 @@
1
+ import { AMBIENT_SOURCES, ambientReadsIn } from './ambientReadsIn.ts'
2
+ import { argsDefaultsIn } from './argsDefaultsIn.ts'
3
+ import {
4
+ byPlace,
5
+ type Diagnosis,
6
+ type Finding,
7
+ placedFindings,
8
+ } from './Diagnostics.ts'
9
+ import { projectShadow } from './projectShadow.ts'
10
+ import type { ParsedSource } from './readSources.ts'
11
+ import type { ShadowHost } from './ShadowHost.ts'
12
+ import { ShadowMap } from './ShadowMap.ts'
13
+
14
+ /**
15
+ * What the build refuses and warns on in the app's `.ts` files and its `.abide` `sources`.
16
+ *
17
+ * 11.37 — `ambient`: every read of a request's ambient inside a `global` value's body, as errors.
18
+ * 11.30 — `unkeyed`: every `args` defaulting a member its body's parameter does not, as warnings.
19
+ * Both over every `.ts` under `within` and each of `sources` as the shadow its reads registered
20
+ * for it, because a keyed value is declared in a `.ts` as often as in a `.abide` — one walk of
21
+ * the files for the two, each finding on its own line of the file an author wrote. `scripts`
22
+ * as a diagnosis reads it: an editor asking about one page does not walk the app's `.ts`.
23
+ */
24
+ export async function appLints(
25
+ host: ShadowHost,
26
+ within: string,
27
+ sources: readonly Omit<ParsedSource, 'quiet'>[],
28
+ scripts: boolean,
29
+ ): Promise<{ ambient: Diagnosis[]; unkeyed: Diagnosis[] }> {
30
+ const ambient: Diagnosis[] = []
31
+ const unkeyed: Diagnosis[] = []
32
+ await host.ask(async (project) => {
33
+ const checker = project.checker
34
+ const ambientPaths = new Map<string, string>()
35
+ const declared = await Promise.all(
36
+ AMBIENT_SOURCES.map((source) =>
37
+ project.program.getSourceFile(source),
38
+ ),
39
+ )
40
+ for (let at = 0; at < AMBIENT_SOURCES.length; at += 1) {
41
+ const found = declared[at]?.path
42
+ if (found !== undefined)
43
+ ambientPaths.set(AMBIENT_SOURCES[at] as string, found)
44
+ }
45
+ const authored = new Map<
46
+ string,
47
+ { source: (typeof sources)[number]; map: ShadowMap }
48
+ >()
49
+ for (const source of sources) {
50
+ const map = new ShadowMap(
51
+ host.canonical(source.path),
52
+ projectShadow(source.file).origins,
53
+ )
54
+ authored.set(host.keyOf(map.shadowPath), { source, map })
55
+ }
56
+ // A file of the host's own is one of `sources`' shadows or not walked at all: a checked
57
+ // module is the lowering's, and placed nowhere an author wrote.
58
+ const files: string[] = []
59
+ if (scripts)
60
+ for (const path of host.rootsUnder(project, within)) {
61
+ if (authored.has(host.keyOf(path)) || !host.holds(path))
62
+ files.push(path)
63
+ }
64
+ else for (const { map } of authored.values()) files.push(map.shadowPath)
65
+ await Promise.all(
66
+ files.map(async (path) => {
67
+ const sourceFile = await project.program.getSourceFile(path)
68
+ if (sourceFile === undefined) return
69
+ const [reads, defaults] = await Promise.all([
70
+ ambientPaths.size === 0
71
+ ? []
72
+ : ambientReadsIn(checker, sourceFile, ambientPaths),
73
+ argsDefaultsIn(checker, sourceFile),
74
+ ])
75
+ if (reads.length === 0 && defaults.length === 0) return
76
+ const shadow = authored.get(host.keyOf(path))
77
+ const source = shadow?.source
78
+ const text =
79
+ source === undefined
80
+ ? await Bun.file(path).text()
81
+ : source.text
82
+ const place = (at: number): number =>
83
+ shadow === undefined ? at : shadow.map.toSource(path, at)
84
+ const named = source?.path ?? path
85
+ const ambientReads: Finding[] = []
86
+ for (const read of reads)
87
+ ambientReads.push({
88
+ at: read.at,
89
+ message: `abide: a \`global\` value's body reads \`${read.ambient}\`, which belongs to the request it was built in — every later request would be answered with this one's. Take it out of the body, or drop \`global\`. RULEBOOK 11.37.`,
90
+ })
91
+ ambient.push(
92
+ ...placedFindings(
93
+ named,
94
+ text,
95
+ ambientReads,
96
+ place,
97
+ 'error',
98
+ ),
99
+ )
100
+ unkeyed.push(
101
+ ...placedFindings(named, text, defaults, place, 'warning'),
102
+ )
103
+ }),
104
+ )
105
+ })
106
+ ambient.sort(byPlace)
107
+ unkeyed.sort(byPlace)
108
+ return { ambient, unkeyed }
109
+ }
@@ -17,7 +17,7 @@ import {
17
17
  } from 'typescript/unstable/ast/is'
18
18
  import { type Checker, SymbolFlags, type Type } from 'typescript/unstable/async'
19
19
 
20
- import type { Finding } from './Finding.ts'
20
+ import type { Finding } from './Diagnostics.ts'
21
21
  import { optionNamed, reactiveOptions } from './reactiveOptions.ts'
22
22
 
23
23
  // 11.30 — AN `args` SCHEMA DEFAULTING A MEMBER THE BODY'S PARAMETER DOES NOT. The key is taken before
@@ -1,15 +1,14 @@
1
1
  import { relative } from 'node:path' // bun exposes no path helpers of its own
2
2
 
3
- import { type Placement, placement } from '#server/placement.ts'
3
+ import { placement } from '#server/placement.ts'
4
4
 
5
- import type { PlacedSource } from './appLayout.ts'
6
- import { type AppChecker, compiled } from './compileApp.ts'
5
+ import { type PlacedSource, placedSource } from './appLayout.ts'
6
+ import { type AppChecker, compiled, compiledSources } from './compileApp.ts'
7
7
  import { abideImportsOf } from './componentGraph.ts'
8
+ import { type Diagnosis, Diagnostics } from './Diagnostics.ts'
8
9
  import { hooksIn } from './hookCalls.ts'
9
10
  import { parseAbide, scriptBodies } from './parseAbide.ts'
10
11
  import { parseSources, readSources } from './readSources.ts'
11
- import { Refusals } from './Refusals.ts'
12
- import type { Diagnosis } from './TypeSession.ts'
13
12
 
14
13
  /**
15
14
  * Every finding `abide check` reports for the app at `appRoot`.
@@ -50,74 +49,71 @@ export async function checkSource(
50
49
  ): Promise<Diagnosis[]> {
51
50
  const { session, root } = checker
52
51
  const layout = await checker.layout()
52
+ const diagnostics = new Diagnostics(root, true)
53
53
  // 38.33 — WHAT THE TREE REFUSES OF IT, off the layout the checker keeps until a move is heard.
54
54
  // What the parser refuses is the held text's, below, never the disk's (`layout.unparsed`).
55
- const tree: Diagnosis[] = []
56
- for (const refused of layout.refused)
57
- if (refused.path === path) tree.push(refused)
58
- const refusals = new Refusals(root, true)
59
- const [page] = parseSources([{ path, text }], refusals)
60
- if (page === undefined) return [...tree, ...refusals.found]
55
+ diagnostics.adopt(layout.refused)
56
+ const [page] = parseSources([{ path, text }], diagnostics)
61
57
  // 23.19 — WHERE THE APP PLACES IT, so a mounted page's `route.params` is typed by the route its
62
58
  // mount gives it, as `checkApp` types it; a file the layout has not seen is placed at its path.
63
- const placed: Pick<Placement, 'kind' | 'route'> =
64
- layout.sources.find((source) => source.path === path) ??
65
- placement(relative(root, path))
66
- const imports: { path: string; text: string; quiet: boolean }[] = []
67
- for (const imported of abideImportsOf(
68
- path,
69
- page.file,
70
- layout.resolve,
71
- ).values())
72
- try {
73
- imports.push({
74
- path: imported,
75
- text: held(imported) ?? (await Bun.file(imported).text()),
76
- quiet: true,
77
- })
78
- } catch {
79
- // A component that cannot be read takes anything here.
80
- }
81
- // 27.13 — AND EVERY FILE FILING `onConfig`, quietly, so a page that only reads `config()` is
82
- // typed by defaults it never imported — as `checkApp`'s whole-app program types it. The calls
83
- // only name candidates; the checker decides which file.
84
- const lowered = new Set(imports.map((one) => one.path))
85
- for (const other of layout.sources) {
86
- if (other.path === path || lowered.has(other.path)) continue
87
- const editing = held(other.path)
88
- if (
89
- editing === undefined || editing === other.text
90
- ? other.hooks.includes('onConfig')
91
- : filesConfig(editing)
59
+ const own: PlacedSource[] = []
60
+ const beside: { path: string; text: string; quiet: boolean }[] = []
61
+ if (page !== undefined) {
62
+ const laid = layout.sources.find((source) => source.path === path)
63
+ own.push(
64
+ laid === undefined
65
+ ? placedSource(page, placement(relative(root, path)), false)
66
+ : { ...laid, ...page },
92
67
  )
93
- imports.push({
94
- path: other.path,
95
- text: editing ?? other.text,
96
- quiet: true,
97
- })
68
+ for (const imported of abideImportsOf(
69
+ path,
70
+ page.file,
71
+ layout.resolve,
72
+ ).values())
73
+ try {
74
+ beside.push({
75
+ path: imported,
76
+ text: held(imported) ?? (await Bun.file(imported).text()),
77
+ quiet: true,
78
+ })
79
+ } catch {
80
+ // A component that cannot be read takes anything here.
81
+ }
82
+ // 27.13 — AND EVERY FILE FILING `onConfig`, quietly, so a page that only reads `config()`
83
+ // is typed by defaults it never imported — as `checkApp`'s whole-app program types it. The
84
+ // calls only name candidates; the checker decides which file.
85
+ const lowered = new Set(beside.map((one) => one.path))
86
+ for (const other of layout.sources) {
87
+ if (other.path === path || lowered.has(other.path)) continue
88
+ const editing = held(other.path)
89
+ if (
90
+ editing === undefined || editing === other.text
91
+ ? other.hooks.includes('onConfig')
92
+ : filesConfig(editing)
93
+ )
94
+ beside.push({
95
+ path: other.path,
96
+ text: editing ?? other.text,
97
+ quiet: true,
98
+ })
99
+ }
98
100
  }
99
101
  // A component that does not parse, or that the checker refuses, is its own file's finding and
100
102
  // takes anything here: a quiet source's refusal is reported by nobody.
101
- const { read, warnings } = await readSources(
102
- session,
103
- [page, ...parseSources(imports, refusals)],
104
- refusals,
105
- )
106
- if (refusals.found.length > 0) return [...tree, ...refusals.found]
107
- const own: Diagnosis[] = []
108
- for (const warning of warnings) if (warning.path === path) own.push(warning)
109
- const [checked, ...beside] = read
110
- const { diagnoses } = await session.diagnose(
111
- checked === undefined
112
- ? beside
113
- : [
114
- { ...checked, kind: placed.kind, route: placed.route },
115
- ...beside,
116
- ],
117
- layout,
118
- false,
119
- )
120
- return [...tree, ...own, ...diagnoses]
103
+ if (own.length > 0)
104
+ await compiledSources(
105
+ layout,
106
+ session,
107
+ own,
108
+ parseSources(beside, diagnostics),
109
+ diagnostics,
110
+ true,
111
+ false,
112
+ )
113
+ const found: Diagnosis[] = []
114
+ for (const finding of diagnostics.findings)
115
+ if (finding.path === path) found.push(finding)
116
+ return found
121
117
  }
122
118
 
123
119
  /**
@@ -132,7 +128,7 @@ export async function lowerApp(
132
128
  held: (path: string) => string | undefined,
133
129
  ): Promise<void> {
134
130
  const layout = await checker.layout()
135
- const refusals = new Refusals(checker.root, true)
131
+ const diagnostics = new Diagnostics(checker.root, true)
136
132
  const sources: (PlacedSource & { quiet: true })[] = []
137
133
  for (const source of layout.sources) {
138
134
  const editing = held(source.path)
@@ -142,12 +138,12 @@ export async function lowerApp(
142
138
  }
143
139
  const [parsed] = parseSources(
144
140
  [{ path: source.path, text: editing, quiet: true }],
145
- refusals,
141
+ diagnostics,
146
142
  )
147
143
  if (parsed !== undefined)
148
144
  sources.push({ ...source, ...parsed, quiet: true })
149
145
  }
150
- const { read } = await readSources(checker.session, sources, refusals)
146
+ const read = await readSources(checker.session, sources, diagnostics)
151
147
  await checker.session.diagnose(read, layout, false)
152
148
  }
153
149
 
@@ -1,7 +1,7 @@
1
1
  import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
2
2
 
3
3
  import type { CheckerAnswer } from './CheckerAnswer.ts'
4
- import type { Finding } from './Finding.ts'
4
+ import type { Finding } from './Diagnostics.ts'
5
5
  import { hookCalls } from './hookCalls.ts'
6
6
  import {
7
7
  CHECKED_CONFIG,
@@ -40,7 +40,7 @@ export type CheckedShadow = {
40
40
 
41
41
  /**
42
42
  * What a checked module's default export is declared as. Scaffolding, so a jump to it is a jump to
43
- * the component's file (`TypeSession.inspect`).
43
+ * the component's file (`LoweredFile.inspect`).
44
44
  */
45
45
  export const COMPONENT_STAND_IN = '__component'
46
46
 
@@ -46,7 +46,8 @@ export type Read = {
46
46
  * `value` lowers to `n()`; `asWritten` stays as written (`n.set(v)`, 31.5); `member` is one of
47
47
  * the `Reactive`'s own members referenced rather than called, named by `path`; `call` is the
48
48
  * explicit `n()` (31.7), which `arguments` says was given any; `shorthand` is `{ n }`;
49
- * `write` and `update` are 31.4, and 31.8 where `path` is not empty.
49
+ * `write` and `update` are 31.4, and 31.8 where `path` is not empty. `mutate` is 31.9: an
50
+ * array mutator called through `path`, its `value` the call from the receiver's end.
50
51
  *
51
52
  * 33's five: the `props<Props>()` call, which lowers to the record the caller passed; the pair
52
53
  * that brackets a binding element's initializer, which together make a default an accessor;
@@ -63,6 +64,7 @@ export type Read = {
63
64
  | 'shorthand'
64
65
  | 'write'
65
66
  | 'update'
67
+ | 'mutate'
66
68
  | 'props'
67
69
  | 'defaultOpen'
68
70
  | 'defaultClose'
@@ -244,6 +246,7 @@ export function classifyPropRead(node: Node): Read | undefined {
244
246
  export function classifyRead(
245
247
  node: Node,
246
248
  valueMember = false,
249
+ mutator = false,
247
250
  ): Read | undefined {
248
251
  const parent = node.parent
249
252
  if (isNotARead(parent)) return undefined
@@ -325,6 +328,18 @@ export function classifyRead(
325
328
  top = up
326
329
  }
327
330
 
331
+ // 31.9 — the walk took the mutator's own name as a step: it is the call, not the path.
332
+ if (
333
+ mutator &&
334
+ isPropertyAccessExpression(top) &&
335
+ isCallExpression(top.parent)
336
+ ) {
337
+ read.path.pop()
338
+ read.shape = 'mutate'
339
+ read.to = top.parent.end
340
+ read.value = { start: top.expression.end, end: top.parent.end }
341
+ return read
342
+ }
328
343
  const outer = top.parent
329
344
  if (isAssigned(outer, top)) {
330
345
  read.shape = 'write'