@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
@@ -7,6 +7,7 @@ import {
7
7
  isBinaryExpression,
8
8
  isCallExpression,
9
9
  isClassDeclaration,
10
+ isElementAccessExpression,
10
11
  isEnumDeclaration,
11
12
  isExpressionStatement,
12
13
  isForOfStatement,
@@ -32,9 +33,11 @@ import {
32
33
  SymbolFlags,
33
34
  type Type,
34
35
  TypeFlags,
36
+ type TypeReference,
35
37
  } from 'typescript/unstable/async'
36
38
 
37
39
  import { aliasedSymbol } from './aliasedSymbol.ts'
40
+ import { ARRAY_MUTATORS } from './ARRAY_MUTATORS.ts'
38
41
  import { CheckerAnswer } from './CheckerAnswer.ts'
39
42
  import {
40
43
  classifyPropRead,
@@ -45,7 +48,7 @@ import {
45
48
  type Read,
46
49
  TRANSPARENT,
47
50
  } from './classifyRead.ts'
48
- import type { Finding } from './Finding.ts'
51
+ import type { Finding } from './Diagnostics.ts'
49
52
  import type { AbideFile } from './parseAbide.ts'
50
53
  import {
51
54
  holdsReactive,
@@ -53,12 +56,13 @@ import {
53
56
  type PositionKind,
54
57
  type PositionTable,
55
58
  } from './PositionTable.ts'
56
- import { INLINE_PROPS, type Span } from './projectShadow.ts'
59
+ import { INLINE_PROPS, type Projection, type Span } from './projectShadow.ts'
57
60
  import { REACTIVE_MEMBERS } from './REACTIVE_MEMBERS.ts'
58
61
  import { optionNamed, reactiveOptions } from './reactiveOptions.ts'
59
62
  import {
60
63
  type Abide,
61
64
  boundInitializer,
65
+ isReactiveSymbol,
62
66
  isReactiveType,
63
67
  unionMembers,
64
68
  valueTypeOf,
@@ -66,19 +70,6 @@ import {
66
70
  import { sharedModuleWrites } from './sharedModuleWrites.ts'
67
71
  import { trackingWarnings } from './trackingWarnings.ts'
68
72
 
69
- /** Where in a `.abide`'s shadow each kind of question is asked (`projectShadow`). */
70
- export type ShadowSpans = {
71
- /** Where the shadow's scripts end and its template begins. */
72
- scriptsEnd: number
73
- /** The `<script module>`, `<script server>` and `<script>` bodies, in the shadow. */
74
- modules: Span
75
- servers: Span
76
- scripts: Span
77
- positions: PositionTable
78
- /** 32.67 — where each `{#for}` row's item binding starts. */
79
- items: Set<number>
80
- }
81
-
82
73
  const SETUP: ReadonlySet<PositionKind> = new Set(['setup'])
83
74
  const HEAD: ReadonlySet<PositionKind> = new Set(['head'])
84
75
  /**
@@ -121,19 +112,22 @@ const RAW_LOWERED: ReadonlySet<PositionKind> = new Set([...PROVABLE, 'prop'])
121
112
  * absent. What each reference DOES — a read, a write, an update, a shorthand — is read off its
122
113
  * parents by `classifyRead`, so no token walk re-derives the grammar.
123
114
  *
124
- * Asked of one snapshot (`TypeSession.readsFor`), and every offset — a read's and a warning's — is
125
- * the shadow's; the session places them in the `.abide`.
115
+ * Asked of one snapshot (`reactiveReads`), and every offset — a read's and a warning's — is the
116
+ * shadow's; `reactiveReads` places them in the `.abide`. `suspects` are where each binding starts
117
+ * whose type the projection cannot vouch for (`bindingsOfReads`), for the next round to ask.
126
118
  */
127
119
  export async function readsIn(
128
120
  checker: Checker,
129
121
  abide: Abide,
130
122
  sourceFile: SourceFile,
131
123
  shadowPath: string,
132
- spans: ShadowSpans,
124
+ projection: Projection,
133
125
  file: AbideFile,
134
126
  /** Where each name the lowered program proved a `Reactive` starts (`bindingsOfReads`). */
135
127
  held: ReadonlySet<number>,
136
128
  ): Promise<{ answer: CheckerAnswer; warnings: Finding[]; suspects: number[] }> {
129
+ const scriptsEnd = projection.scripts.end
130
+ const positions = projection.positions
137
131
  // A destructuring pattern binds names this does not see, and that is 31.12's case.
138
132
  const names: Node[] = []
139
133
  const spelled = new Set<string>()
@@ -143,7 +137,7 @@ export async function readsIn(
143
137
  names.push(name)
144
138
  }
145
139
  for (const statement of sourceFile.statements) {
146
- if (statement.getStart() >= spans.scriptsEnd) break
140
+ if (statement.getStart() >= scriptsEnd) break
147
141
  if (isVariableStatement(statement)) {
148
142
  for (const declaration of statement.declarationList.declarations)
149
143
  bind(declaration.name)
@@ -172,12 +166,11 @@ export async function readsIn(
172
166
  const topLevel = names.length
173
167
  // The script's own statements, as the top-level walk takes the page's: a declaration inside
174
168
  // one of its functions is not a name the body reads.
175
- for (const { node } of spans.positions.nodesIn(sourceFile, SETUP))
169
+ for (const { node } of positions.nodesIn(sourceFile, SETUP))
176
170
  if (isVariableStatement(node))
177
171
  for (const declaration of node.declarationList.declarations)
178
172
  if (isIdentifier(declaration.name)) names.push(declaration.name)
179
- refuseDeclared(sourceFile.statements, spans.scriptsEnd, file)
180
- const reads: Read[] = []
173
+ refuseDeclared(sourceFile.statements, scriptsEnd, file)
181
174
  // 33's pass runs whatever 31.3's found: a `props` pattern binds no name the walk
182
175
  // above collects — `const { amount } = props()` declares a PATTERN — so a file
183
176
  // whose only declaration is one would have returned with its props unread.
@@ -186,22 +179,21 @@ export async function readsIn(
186
179
  abide,
187
180
  sourceFile,
188
181
  shadowPath,
189
- spans.scriptsEnd,
190
- spans.positions,
191
- reads,
182
+ scriptsEnd,
183
+ positions,
192
184
  )
193
185
  // Awaited after the round trips below, so a 33.14 refusal would reject with nothing
194
186
  // listening and be reported unhandled; `await props` still throws it.
195
187
  props.catch(() => {})
196
- const params = routeReads(checker, abide, sourceFile, reads)
197
- const rows = rowReads(checker, sourceFile, shadowPath, spans, reads)
198
- const configs = configReads(
188
+ const params = routeReads(checker, abide, sourceFile)
189
+ const rows = rowReads(
199
190
  checker,
200
- abide,
201
191
  sourceFile,
202
- spans.servers,
203
- reads,
192
+ shadowPath,
193
+ positions,
194
+ new Set(projection.items.values()),
204
195
  )
196
+ const configs = configReads(checker, abide, sourceFile, projection.servers)
205
197
  // No name is no round trip to ask about none.
206
198
  const symbols =
207
199
  names.length === 0 ? [] : await checker.getSymbolAtLocation(names)
@@ -249,30 +241,34 @@ export async function readsIn(
249
241
  const owned = await ownMembers(checker, nodes.flat(), (node) =>
250
242
  valueTypeOf(checker, node),
251
243
  )
244
+ const lifted = await liftedMutators(checker, nodes.flat(), (node) =>
245
+ valueTypeOf(checker, node),
246
+ )
252
247
  const holding: Promise<void>[] = []
253
248
  const located: { node: Node; read: Read }[] = []
249
+ const named: Read[] = []
254
250
  for (let index = 0; index < nodes.length; index += 1)
255
251
  for (const node of nodes[index] as (Node | undefined)[]) {
256
252
  if (node === undefined) continue
257
- const read = classifyRead(node, owned.has(node))
253
+ const read = classifyRead(node, owned.has(node), lifted.has(node))
258
254
  if (read === undefined) continue
259
255
  read.byValue = byValue[index] as boolean
260
256
  read.local = index >= topLevel
261
- reads.push(read)
257
+ named.push(read)
262
258
  located.push({ node, read })
263
259
  if (read.shape === 'value')
264
260
  holding.push(holdIn(checker, abide, node, read))
265
261
  }
266
262
  await Promise.all(holding)
267
- await props
268
- await params
269
- await rows
270
- await configs
263
+ const reads = [
264
+ ...(await props),
265
+ ...(await params),
266
+ ...(await rows),
267
+ ...(await configs),
268
+ ...named,
269
+ ]
271
270
  const statics = new Set<number>()
272
- const expressions = spans.positions.nodesIn(
273
- sourceFile,
274
- TEMPLATE_EXPRESSIONS,
275
- )
271
+ const expressions = positions.nodesIn(sourceFile, TEMPLATE_EXPRESSIONS)
276
272
  const rawCallees: Node[] = []
277
273
  for (const { position, node } of expressions)
278
274
  expressionFacts(
@@ -281,15 +277,15 @@ export async function readsIn(
281
277
  )
282
278
  // A `Reactive` the page named `raw` is a read, and the read's rewrite is the answer.
283
279
  if (rawCallees.length > 0) {
284
- const named = new Set<number>()
285
- for (const one of reads) named.add(one.start)
280
+ const starts = new Set<number>()
281
+ for (const one of reads) starts.add(one.start)
286
282
  for (const callee of rawCallees)
287
- if (!named.has(callee.getStart()))
283
+ if (!starts.has(callee.getStart()))
288
284
  reads.push(newRead(callee.getStart(), callee.end, 'raw'))
289
285
  }
290
286
  await proveSlots(checker, expressions, reads, statics)
291
287
  reads.sort(byPosition)
292
- const warnings = await warningsFor(checker, located, spans.positions)
288
+ const warnings = await warningsFor(checker, located, positions)
293
289
  // 34.18.
294
290
  for (const warning of await sharedModuleWrites(
295
291
  checker,
@@ -297,8 +293,8 @@ export async function readsIn(
297
293
  names.slice(0, topLevel),
298
294
  symbols,
299
295
  reactive,
300
- spans.modules,
301
- spans.scripts,
296
+ projection.modules,
297
+ projection.scripts,
302
298
  ))
303
299
  warnings.push(warning)
304
300
  const answer = new CheckerAnswer(reads, statics)
@@ -317,7 +313,7 @@ export async function readsIn(
317
313
 
318
314
  /**
319
315
  * Where each binding starts whose type the projection cannot vouch for, for the lowered program to
320
- * answer (`TypeSession.classify`).
316
+ * answer (`reactiveReads`).
321
317
  *
322
318
  * 31.1 — A BINDING TYPED FROM A READ IS TYPED FROM THE `Reactive` IN THE PROJECTION, and the read is
323
319
  * of its value. `reactive(route.url.pathname)` asks a `Reactive<URL>` for a `pathname` it does not
@@ -549,7 +545,7 @@ function annotated(
549
545
  async function proveSlots(
550
546
  checker: Checker,
551
547
  expressions: readonly { position: Position; node: Node }[],
552
- reads: Read[],
548
+ reads: readonly Read[],
553
549
  /** Where each position it proves static starts, added to. */
554
550
  statics: Set<number>,
555
551
  ): Promise<void> {
@@ -761,9 +757,9 @@ async function routeReads(
761
757
  checker: Checker,
762
758
  abide: Abide,
763
759
  sourceFile: Node,
764
- reads: Read[],
765
- ): Promise<void> {
766
- if (abide.route === undefined) return
760
+ ): Promise<Read[]> {
761
+ const reads: Read[] = []
762
+ if (abide.route === undefined) return reads
767
763
  const accesses: Node[] = []
768
764
  const owners: Node[] = []
769
765
  const visit = (node: Node): void => {
@@ -779,7 +775,7 @@ async function routeReads(
779
775
  node.forEachChild(visit)
780
776
  }
781
777
  sourceFile.forEachChild(visit)
782
- if (owners.length === 0) return
778
+ if (owners.length === 0) return reads
783
779
  const symbols = await checker.getSymbolAtLocation(owners)
784
780
  for (let at = 0; at < accesses.length; at += 1) {
785
781
  const symbol = symbols[at]
@@ -794,6 +790,7 @@ async function routeReads(
794
790
  (access as { name?: { text: string } }).name?.text === 'params'
795
791
  reads.push(read)
796
792
  }
793
+ return reads
797
794
  }
798
795
 
799
796
  /**
@@ -810,10 +807,12 @@ async function rowReads(
810
807
  checker: Checker,
811
808
  sourceFile: Node,
812
809
  shadowPath: string,
813
- spans: ShadowSpans,
814
- reads: Read[],
815
- ): Promise<void> {
816
- if (spans.items.size === 0) return
810
+ positions: PositionTable,
811
+ /** Where each `{#for}` row's item binding starts. */
812
+ items: Set<number>,
813
+ ): Promise<Read[]> {
814
+ const reads: Read[] = []
815
+ if (items.size === 0) return reads
817
816
  const bound: Node[] = []
818
817
  const loops: Node[] = []
819
818
  const bind = (name: Node | undefined, loop: Node): void => {
@@ -828,12 +827,12 @@ async function rowReads(
828
827
  bind((element as { name?: Node }).name, loop)
829
828
  }
830
829
  const visit = (node: Node): void => {
831
- const declared = rowItemOf(node, spans.items)
830
+ const declared = rowItemOf(node, items)
832
831
  if (declared !== undefined) bind(declared, node)
833
832
  node.forEachChild(visit)
834
833
  }
835
834
  sourceFile.forEachChild(visit)
836
- if (bound.length === 0) return
835
+ if (bound.length === 0) return reads
837
836
  const symbols = await checker.getSymbolAtLocation(bound)
838
837
  const found = await Promise.all(
839
838
  symbols.map(async (symbol) =>
@@ -852,12 +851,12 @@ async function rowReads(
852
851
  for (let at = 0; at < found.length; at += 1) {
853
852
  const name = bound[at] as Node
854
853
  const loop = loops[at] as Node
855
- const item = (rowItemOf(loop, spans.items) as Node).getStart()
854
+ const item = (rowItemOf(loop, items) as Node).getStart()
856
855
  for (const node of found[at] as (Node | undefined)[]) {
857
856
  if (node === undefined || node === name) continue
858
857
  if (
859
- spans.positions.holding(node.getStart())?.kind === 'key' &&
860
- rowLoopOf(node, spans.items) === loop
858
+ positions.holding(node.getStart())?.kind === 'key' &&
859
+ rowLoopOf(node, items) === loop
861
860
  )
862
861
  continue
863
862
  const read = classifyPropRead(node)
@@ -866,6 +865,7 @@ async function rowReads(
866
865
  reads.push(read)
867
866
  }
868
867
  }
868
+ return reads
869
869
  }
870
870
 
871
871
  /** The item binding `node` declares, where it is a `{#for}` row loop. */
@@ -933,7 +933,7 @@ function refuseDeclared(
933
933
 
934
934
  /**
935
935
  * 27.13 — each `onConfig(…)` standing as a statement at the top of a `<script server>`, as a read of
936
- * its callee, pushed onto `reads`.
936
+ * its callee.
937
937
  *
938
938
  * The callee says which are abide's, as `props`'s does. A call anywhere else — inside a function,
939
939
  * or handing its unfiling back to a name — is filed all the same, and declares nothing.
@@ -943,9 +943,9 @@ async function configReads(
943
943
  abide: Abide,
944
944
  sourceFile: Node & { statements: readonly Node[] },
945
945
  servers: Span,
946
- reads: Read[],
947
- ): Promise<void> {
948
- if (abide.onConfig === undefined) return
946
+ ): Promise<Read[]> {
947
+ const reads: Read[] = []
948
+ if (abide.onConfig === undefined) return reads
949
949
  const callees: Node[] = []
950
950
  for (const statement of sourceFile.statements) {
951
951
  const start = statement.getStart()
@@ -958,7 +958,7 @@ async function configReads(
958
958
  )
959
959
  callees.push(statement.expression.expression)
960
960
  }
961
- if (callees.length === 0) return
961
+ if (callees.length === 0) return reads
962
962
  const named = await checker.getSymbolAtLocation(callees)
963
963
  for (let at = 0; at < callees.length; at += 1) {
964
964
  const symbol = named[at]
@@ -972,10 +972,11 @@ async function configReads(
972
972
  const node = callees[at] as Node
973
973
  reads.push(newRead(node.getStart(), node.end, 'config'))
974
974
  }
975
+ return reads
975
976
  }
976
977
 
977
978
  /**
978
- * The reads a `props<Props>()` destructuring contributes, pushed onto `reads`.
979
+ * The reads a `props<Props>()` destructuring contributes.
979
980
  *
980
981
  * 33 — WHAT `props<Props>()` BINDS, and it is a second question from 31.3's rather than a case of
981
982
  * it: a prop is not a `Reactive` and is not typed as one, so no type answers this. The CALLEE
@@ -997,8 +998,8 @@ async function propsReads(
997
998
  shadowPath: string,
998
999
  scriptsEnd: number,
999
1000
  positions: PositionTable,
1000
- reads: Read[],
1001
- ): Promise<void> {
1001
+ ): Promise<Read[]> {
1002
+ const reads: Read[] = []
1002
1003
  // The spelling only says which calls are worth asking about; the callee says which are abide's.
1003
1004
  const calls: { pattern: Node; call: Node; local: boolean }[] = []
1004
1005
  const callees: Node[] = []
@@ -1024,7 +1025,7 @@ async function propsReads(
1024
1025
  // 32.40 — AND EACH INLINE COMPONENT'S HEAD, inside the function it is projected as. Its names
1025
1026
  // are that function's, so a read of one is local as a branch-local name is (14.8).
1026
1027
  for (const { node } of positions.nodesIn(sourceFile, HEAD)) take(node, true)
1027
- if (calls.length === 0 || abide.props === undefined) return
1028
+ if (calls.length === 0 || abide.props === undefined) return reads
1028
1029
  const named = await checker.getSymbolAtLocation(callees)
1029
1030
  // One entry per name a pattern binds: the key it takes, the call's read its key joins where
1030
1031
  // the child writes it, and its default's pair.
@@ -1091,7 +1092,7 @@ async function propsReads(
1091
1092
  })
1092
1093
  }
1093
1094
  }
1094
- if (bound.length === 0) return
1095
+ if (bound.length === 0) return reads
1095
1096
 
1096
1097
  const symbols = await checker.getSymbolAtLocation(bound)
1097
1098
  const found = await Promise.all(
@@ -1111,6 +1112,9 @@ async function propsReads(
1111
1112
  const owned = await ownMembers(checker, found.flat(), (node) =>
1112
1113
  checker.getTypeAtLocation(node),
1113
1114
  )
1115
+ const lifted = await liftedMutators(checker, found.flat(), (node) =>
1116
+ checker.getTypeAtLocation(node),
1117
+ )
1114
1118
  // 33.27 — each prop asked whether its position holds the `Reactive`, all at once.
1115
1119
  const holding: Promise<void>[] = []
1116
1120
  for (let at = 0; at < found.length; at += 1) {
@@ -1128,11 +1132,12 @@ async function propsReads(
1128
1132
  node.parent?.kind === SyntaxKind.BindingElement
1129
1133
  )
1130
1134
  continue
1131
- const read = classifyRead(node, owned.has(node))
1135
+ const read = classifyRead(node, owned.has(node), lifted.has(node))
1132
1136
  if (read === undefined) continue
1133
1137
  if (
1134
1138
  read.shape === 'write' ||
1135
1139
  read.shape === 'update' ||
1140
+ read.shape === 'mutate' ||
1136
1141
  isBindOperand(positions, node) ||
1137
1142
  writesThrough(node, owned)
1138
1143
  )
@@ -1178,6 +1183,7 @@ async function propsReads(
1178
1183
  }
1179
1184
  }
1180
1185
  await Promise.all(holding)
1186
+ return reads
1181
1187
  }
1182
1188
 
1183
1189
  /**
@@ -1232,6 +1238,108 @@ async function ownMembers(
1232
1238
  return owned
1233
1239
  }
1234
1240
 
1241
+ /**
1242
+ * 31.9 — the occurrences among `nodes` that call an array mutator through a member path, where the
1243
+ * receiver's type is an array: `tags.push(t)`, `deal.notes.splice(0, 1)`. The name says which
1244
+ * method and the type says whose, so a class's own `push` is left a call on the class.
1245
+ *
1246
+ * The receiver's type is walked down from the value's, step by step: in the projection a member of
1247
+ * the name is the `Reactive`'s, which declares none of the value's.
1248
+ */
1249
+ async function liftedMutators(
1250
+ checker: Checker,
1251
+ nodes: readonly (Node | undefined)[],
1252
+ valueOf: (node: Node) => Promise<Type | undefined>,
1253
+ ): Promise<Set<Node>> {
1254
+ const lifted = new Set<Node>()
1255
+ const asked: Promise<void>[] = []
1256
+ for (const node of nodes) {
1257
+ if (node === undefined) continue
1258
+ const steps: (string | undefined)[] = []
1259
+ let receiver: Node = node
1260
+ let mutator = false
1261
+ while (true) {
1262
+ const up = receiver.parent
1263
+ if (isPropertyAccessExpression(up) && up.expression === receiver) {
1264
+ if (
1265
+ ARRAY_MUTATORS.has(up.name.text) &&
1266
+ isCallExpression(up.parent) &&
1267
+ up.parent.expression === up
1268
+ ) {
1269
+ mutator = true
1270
+ break
1271
+ }
1272
+ steps.push(up.name.text)
1273
+ } else if (
1274
+ isElementAccessExpression(up) &&
1275
+ up.expression === receiver
1276
+ )
1277
+ steps.push(undefined)
1278
+ else break
1279
+ receiver = up
1280
+ }
1281
+ if (!mutator) continue
1282
+ asked.push(
1283
+ valueOf(node).then(async (value) => {
1284
+ if (value === undefined) return
1285
+ let types: Type[] = [value]
1286
+ for (const step of steps)
1287
+ types = await stepTypes(checker, types, step)
1288
+ if (types.length === 0) return
1289
+ for (const type of types)
1290
+ if (
1291
+ !(await checker.isArrayType(type)) &&
1292
+ !(await checker.isTupleType(type))
1293
+ )
1294
+ return
1295
+ lifted.add(node)
1296
+ }),
1297
+ )
1298
+ }
1299
+ await Promise.all(asked)
1300
+ return lifted
1301
+ }
1302
+
1303
+ /**
1304
+ * The types one step down from `types`, `null` and `undefined` aside: a property's by its name, and
1305
+ * an element's or an index signature's where the step is computed.
1306
+ */
1307
+ async function stepTypes(
1308
+ checker: Checker,
1309
+ types: readonly Type[],
1310
+ step: string | undefined,
1311
+ ): Promise<Type[]> {
1312
+ const next: Type[] = []
1313
+ for (const type of types)
1314
+ for (const held of await unionMembers(type)) {
1315
+ if ((held.flags & NULLISH) !== 0) continue
1316
+ if (step !== undefined) {
1317
+ const member = await checker.getPropertyOfType(held, step)
1318
+ const stepped =
1319
+ member === undefined
1320
+ ? undefined
1321
+ : await checker.getTypeOfSymbol(member)
1322
+ if (stepped !== undefined)
1323
+ next.push(...(await unionMembers(stepped)))
1324
+ continue
1325
+ }
1326
+ if (await checker.isArrayType(held)) {
1327
+ const [element] = await checker.getTypeArguments(
1328
+ held as TypeReference,
1329
+ )
1330
+ if (element !== undefined)
1331
+ next.push(...(await unionMembers(element)))
1332
+ continue
1333
+ }
1334
+ for (const index of await checker.getIndexInfosOfType(held))
1335
+ next.push(...(await unionMembers(index.valueType)))
1336
+ }
1337
+ const present: Type[] = []
1338
+ for (const type of next)
1339
+ if ((type.flags & NULLISH) === 0) present.push(type)
1340
+ return present
1341
+ }
1342
+
1235
1343
  /**
1236
1344
  * 31.7, 33.6, 33.7 — `n.set(v)` and `n.patch(…)` are writes by name, called or handed on, where the
1237
1345
  * value's own type declares no member of that name — a `Map`'s `set` writes the map, not the prop.
@@ -1323,15 +1431,6 @@ function isBindOperand(positions: PositionTable, node: Node): boolean {
1323
1431
  return holdsReactive(positions.of(node))
1324
1432
  }
1325
1433
 
1326
- async function isReactiveSymbol(
1327
- checker: Checker,
1328
- abide: Abide,
1329
- symbol: CheckedSymbol,
1330
- ): Promise<boolean> {
1331
- const type = await checker.getTypeOfSymbol(symbol)
1332
- return type !== undefined && isReactiveType(checker, abide, type, false)
1333
- }
1334
-
1335
1434
  /**
1336
1435
  * Orders reads by offset, a default's open before a read at the same offset and its close after.
1337
1436
  *
@@ -0,0 +1,46 @@
1
+ import { parseRoutePattern } from '#shared/routePattern.ts'
2
+
3
+ import type { AppRoute } from './appLayout.ts'
4
+
5
+ const URL_SOURCE = Bun.fileURLToPath(new URL('../shared/url', import.meta.url))
6
+
7
+ /**
8
+ * 30.26 — each of `addresses` as a key of abide's `Routes`, and 23.9, 30.3 — a page route's params
9
+ * narrowed where its matcher is an alternation of literals, which is the one matcher a type can say.
10
+ */
11
+ export function routesDeclaration(
12
+ addresses: readonly string[],
13
+ routes: readonly AppRoute[],
14
+ ): string {
15
+ const narrowed = new Map<string, string>()
16
+ for (const route of routes) {
17
+ let members = ''
18
+ for (const segment of parseRoutePattern(route.pattern)) {
19
+ if (segment.kind !== 'required' && segment.kind !== 'optional')
20
+ continue
21
+ const literals = literalsOf(
22
+ segment.matcher === undefined
23
+ ? undefined
24
+ : route.matchers[segment.matcher],
25
+ )
26
+ if (literals === undefined) continue
27
+ members += ` ${JSON.stringify(segment.name)}${segment.kind === 'optional' ? '?' : ''}: ${literals};`
28
+ }
29
+ if (members !== '') narrowed.set(route.pattern, `{${members} }`)
30
+ }
31
+ let keys = ''
32
+ for (const route of addresses)
33
+ keys += ` ${JSON.stringify(route)}: ${narrowed.get(route) ?? 'true'}\n`
34
+ return `export {}\ndeclare module ${JSON.stringify(URL_SOURCE)} {\n interface Routes {\n${keys} }\n}\n`
35
+ }
36
+
37
+ /** The union of the literals an anchored matcher source alternates between, or `undefined`. */
38
+ function literalsOf(source: string | undefined): string | undefined {
39
+ const body = source?.match(/^\^\(\?!\\\.\\\.\?\$\)\(\?:(.*)\)\$$/)?.[1]
40
+ if (body === undefined || !/^[\w~-]+(?:\|[\w~-]+)*$/.test(body))
41
+ return undefined
42
+ return body
43
+ .split('|')
44
+ .map((literal) => JSON.stringify(literal))
45
+ .join(' | ')
46
+ }
@@ -40,14 +40,19 @@ const SOURCE_ALIAS = { '#src/*': ['./src/*', './src/*.ts'] }
40
40
  * lands first on the PATH inside `bun run`, shadowing the bun that runs everything else.
41
41
  */
42
42
  export const BUNFIG =
43
- '# 38.122 — a peer is not installed: an installed `bun` would shadow this one inside `bun run`.\n[install]\npeer = false\n\n# 38.43, 38.131 — the plugin that builds the stylesheet `src/lib/app.css` opens.\n[serve.static]\nplugins = ["bun-plugin-tailwind"]\n\n# 38.109, 38.119 — a DOM to mount into and measure, then the app\'s `.abide` files as modules.\n[test]\nroot = "src"\npreload = ["@abide/harness/abide/preload"]\n\n# What `bun fmt` and `abide format` print.\n[fmt]\ntabWidth = 4\nsemi = false\nsingleQuote = true\nsortImports = true\n'
43
+ '# 38.122 — no peers: an installed `bun` would shadow this one in `bun run`.\n[install]\npeer = false\n\n# 38.43, 38.131 — the plugin building the stylesheet `src/lib/app.css` opens.\n[serve.static]\nplugins = ["bun-plugin-tailwind"]\n\n# 38.109, 38.119 — a DOM to measure in, then the `.abide` files as modules.\n[test]\nroot = "src"\npreload = ["@abide/harness/abide/preload"]\n\n# What `bun fmt` and `abide format` print.\n[fmt]\ntabWidth = 4\nsemi = false\nsingleQuote = true\nsortImports = true\n'
44
44
 
45
45
  // WRITTEN, NOT COPIED, beside the manifests: a packer drops a `bunfig.toml` or a `.gitignore` from
46
- // the tarball (`bun pm pack` both, `npm pack` the second), so neither survives as a starter file.
46
+ // the tarball (`bun pm pack` both, `npm pack` the second), so neither survives as a starter file —
47
+ // and the Dockerfile names the bun that is running, which no file can.
47
48
  function filesFor(name: string): Record<string, string> {
48
49
  return {
49
50
  'bunfig.toml': BUNFIG,
50
51
  '.gitignore': 'node_modules\n.abide\n',
52
+ // 38.132 — the build stage on the bun that scaffolded the project, the image on nothing but
53
+ // the executable it compiles.
54
+ Dockerfile: `FROM oven/bun:${Bun.version} AS build\nWORKDIR /app\nCOPY package.json bun.lock bunfig.toml ./\nRUN bun install --frozen-lockfile\nCOPY . .\nRUN bun x abide compile --out /app/server\n\nFROM gcr.io/distroless/cc-debian12\nCOPY --from=build /app/server /server\nENV NODE_ENV=production\nEXPOSE 3000\nCMD ["/server", "start"]\n`,
55
+ '.dockerignore': 'node_modules\n.abide\n.git\n.env\n.env.*\n',
51
56
  'package.json': `${JSON.stringify(
52
57
  {
53
58
  name,