@abide/abide 0.118.0 → 0.119.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 (149) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/bin/abide.ts +74 -11
  3. package/docs/REGISTRY.md +2 -1
  4. package/docs/RULEBOOK.md +22 -0
  5. package/docs/abide.md +338 -118
  6. package/package.json +1 -1
  7. package/scaffold/src/getGreeting.test.ts +1 -0
  8. package/scaffold/src/index.abide +4 -1
  9. package/scaffold/src/index.test.ts +2 -0
  10. package/scaffold/src/layout.abide +6 -6
  11. package/src/compiler/Lowering.ts +20 -2
  12. package/src/compiler/PositionTable.ts +1 -0
  13. package/src/compiler/Refusals.ts +1 -0
  14. package/src/compiler/TypeSession.ts +11 -8
  15. package/src/compiler/aliasedSymbol.ts +1 -1
  16. package/src/compiler/ambientReadsIn.ts +1 -0
  17. package/src/compiler/appLayout.ts +4 -2
  18. package/src/compiler/argsDefaultsIn.ts +1 -0
  19. package/src/compiler/argsSchemaOf.ts +1 -0
  20. package/src/compiler/checkApp.ts +3 -1
  21. package/src/compiler/checkedShadow.ts +2 -1
  22. package/src/compiler/classifyRead.ts +1 -0
  23. package/src/compiler/classifyTemplate.ts +1 -0
  24. package/src/compiler/compileApp.ts +4 -2
  25. package/src/compiler/compiledEntry.ts +1 -0
  26. package/src/compiler/componentGraph.ts +1 -0
  27. package/src/compiler/declaredExports.ts +4 -2
  28. package/src/compiler/emitClient.ts +19 -14
  29. package/src/compiler/emitComponent.ts +1 -0
  30. package/src/compiler/emitSource.ts +2 -1
  31. package/src/compiler/formatAbide.ts +857 -0
  32. package/src/compiler/formatDiagnosis.ts +1 -0
  33. package/src/compiler/handlersUnder.ts +2 -1
  34. package/src/compiler/hookCalls.ts +1 -0
  35. package/src/compiler/lineOf.ts +17 -0
  36. package/src/compiler/matchersOf.ts +13 -3
  37. package/src/compiler/mountsOf.ts +2 -0
  38. package/src/compiler/paramFile.ts +1 -0
  39. package/src/compiler/parseAbide.ts +48 -28
  40. package/src/compiler/projectShadow.ts +10 -3
  41. package/src/compiler/reactiveOptions.ts +1 -0
  42. package/src/compiler/reactiveTypes.ts +2 -1
  43. package/src/compiler/readsIn.ts +3 -2
  44. package/src/compiler/renderMarkup.ts +3 -2
  45. package/src/compiler/resolverIn.ts +2 -0
  46. package/src/compiler/sameTemplate.ts +150 -0
  47. package/src/compiler/scaffold.ts +4 -11
  48. package/src/compiler/serveLanguage.ts +46 -19
  49. package/src/compiler/sharedModuleWrites.ts +2 -1
  50. package/src/compiler/sourceMapOf.ts +1 -0
  51. package/src/compiler/splitImports.ts +2 -0
  52. package/src/compiler/templateWarnings.ts +2 -2
  53. package/src/compiler/trackingWarnings.ts +1 -0
  54. package/src/compiler/writeDefaultProject.ts +5 -1
  55. package/src/server/BRIDGE_VERSION.ts +1 -1
  56. package/src/server/abideModules.ts +2 -0
  57. package/src/server/answerFailure.ts +1 -0
  58. package/src/server/answerRequest.ts +15 -14
  59. package/src/server/answerRpc.ts +3 -2
  60. package/src/server/answerSocket.ts +2 -1
  61. package/src/server/answerValue.ts +1 -0
  62. package/src/server/answerWorker.ts +1 -0
  63. package/src/server/appTree.ts +3 -1
  64. package/src/server/argsFor.ts +1 -0
  65. package/src/server/assistant.ts +2 -1
  66. package/src/server/banner.ts +2 -0
  67. package/src/server/bridgeRelay.ts +1 -0
  68. package/src/server/callerOf.ts +1 -0
  69. package/src/server/claudeApi.ts +1 -0
  70. package/src/server/claudeCode.ts +1 -0
  71. package/src/server/clientBundle.ts +1 -0
  72. package/src/server/coerceArgs.ts +1 -2
  73. package/src/server/commands.ts +9 -0
  74. package/src/server/compressAnswer.ts +2 -1
  75. package/src/server/config.ts +2 -0
  76. package/src/server/createApp.ts +4 -2
  77. package/src/server/crossingOrigin.ts +1 -0
  78. package/src/server/csp.ts +1 -0
  79. package/src/server/deferredBuilds.ts +1 -0
  80. package/src/server/derivedArgs.ts +1 -0
  81. package/src/server/devWorker.ts +2 -1
  82. package/src/server/faceOf.ts +2 -7
  83. package/src/server/generatedSurfaces.ts +5 -1
  84. package/src/server/hooks.ts +1 -0
  85. package/src/server/iconAccent.ts +1 -1
  86. package/src/server/installServer.ts +1 -0
  87. package/src/server/installSourceMaps.ts +1 -0
  88. package/src/server/launcherPage.ts +2 -2
  89. package/src/server/manifestOf.ts +1 -0
  90. package/src/server/muxUpgrade.ts +1 -0
  91. package/src/server/nativeMenu.ts +2 -0
  92. package/src/server/principal.ts +2 -1
  93. package/src/server/remote.ts +1 -0
  94. package/src/server/render.ts +3 -3
  95. package/src/server/renderComponent.ts +1 -0
  96. package/src/server/reportHealth.ts +1 -0
  97. package/src/server/requestScope.ts +3 -1
  98. package/src/server/runDesktop.ts +3 -2
  99. package/src/server/seedBuffer.ts +1 -0
  100. package/src/server/serveDev.ts +3 -2
  101. package/src/server/shell.ts +2 -0
  102. package/src/server/streamEvent.ts +1 -0
  103. package/src/server/surfaceOf.ts +1 -0
  104. package/src/server/testBuild.ts +1 -0
  105. package/src/server/testMount.ts +1 -0
  106. package/src/server/trace.ts +2 -0
  107. package/src/server/withAbideSources.ts +1 -0
  108. package/src/shared/Attributes.ts +12 -8
  109. package/src/shared/cookies.ts +1 -1
  110. package/src/shared/escapeText.ts +1 -1
  111. package/src/shared/index.ts +1 -5
  112. package/src/shared/jsonSchema.ts +1 -3
  113. package/src/shared/menuBridge.ts +1 -2
  114. package/src/shared/outbox.ts +1 -1
  115. package/src/shared/reactive/Keyed.ts +2 -2
  116. package/src/shared/reactive/Reactive.ts +12 -16
  117. package/src/shared/reactive/index.ts +3 -4
  118. package/src/shared/reactive/isReactive.ts +1 -1
  119. package/src/shared/reactive/scoped.ts +2 -2
  120. package/src/shared/reactive/share.ts +1 -1
  121. package/src/shared/reactive/watch.ts +4 -4
  122. package/src/shared/refuse.ts +1 -1
  123. package/src/shared/rpc.ts +8 -11
  124. package/src/shared/url.ts +1 -1
  125. package/src/shared/validateJson.ts +1 -1
  126. package/src/ui/ArmRegion.ts +1 -0
  127. package/src/ui/Level.ts +1 -0
  128. package/src/ui/List.ts +1 -0
  129. package/src/ui/RawRun.ts +1 -0
  130. package/src/ui/Router.ts +1 -0
  131. package/src/ui/TryRegion.ts +1 -0
  132. package/src/ui/adoptWalk.ts +2 -1
  133. package/src/ui/bindElement.ts +1 -0
  134. package/src/ui/bindFiles.ts +1 -0
  135. package/src/ui/bindGroup.ts +1 -0
  136. package/src/ui/bindProperty.ts +1 -0
  137. package/src/ui/bindSpread.ts +1 -0
  138. package/src/ui/index.ts +1 -6
  139. package/src/ui/outboxQueue.ts +1 -1
  140. package/src/ui/parseMarkup.ts +2 -1
  141. package/src/ui/reconcile.ts +2 -1
  142. package/src/ui/reconcileHead.ts +1 -0
  143. package/src/ui/regions.ts +2 -1
  144. package/src/ui/registerWorker.ts +1 -0
  145. package/src/ui/rows.ts +1 -0
  146. package/src/ui/setMarkup.ts +1 -0
  147. package/src/ui/setText.ts +1 -0
  148. package/src/ui/socketWire.ts +4 -3
  149. package/src/ui/wire.ts +2 -1
@@ -1,4 +1,5 @@
1
1
  import { relative } from 'node:path' // bun exposes no path helpers of its own
2
+
2
3
  import type { Diagnosis } from './TypeSession.ts'
3
4
 
4
5
  /**
@@ -11,12 +11,13 @@
11
11
 
12
12
  import type { Mount } from '#server/answerRpc.ts'
13
13
  import { frameworkRouteRefusal } from '#server/appTree.ts'
14
+ import { PRIVATE_EXPORT } from '#server/PRIVATE_EXPORT.ts'
14
15
  import { surfaceName } from '#server/surfaceOf.ts'
15
16
  import { PRIVATE_HANDLERS_ADDRESS } from '#shared/ADDRESSES.ts'
16
17
  import { parseRoutePattern } from '#shared/routePattern.ts'
18
+
17
19
  import type { PlacedHandler, PlacedSource } from './appLayout.ts'
18
20
  import { declaredExports } from './declaredExports.ts'
19
- import { PRIVATE_EXPORT } from '#server/PRIVATE_EXPORT.ts'
20
21
  import type { Refusals } from './Refusals.ts'
21
22
  import type { Diagnosis, TypeSession } from './TypeSession.ts'
22
23
 
@@ -1,4 +1,5 @@
1
1
  import { SHARED_SPECIFIER } from '#shared/SPECIFIERS.ts'
2
+
2
3
  import { importStatements } from './splitImports.ts'
3
4
  import { type Token, tokenize } from './tokenize.ts'
4
5
 
@@ -20,6 +20,23 @@ export function lineOf(
20
20
  return { line, column: end - lineStart + 1 }
21
21
  }
22
22
 
23
+ /**
24
+ * The protocol's `{ line, character }`, from 0, as an offset into `text`. A character is a UTF-16
25
+ * code unit in the protocol and in a JavaScript string alike.
26
+ */
27
+ export function offsetOf(
28
+ text: string,
29
+ position: { line: number; character: number },
30
+ ): number {
31
+ let at = 0
32
+ for (let line = 0; line < position.line; line += 1) {
33
+ const end = text.indexOf('\n', at)
34
+ if (end === -1) return text.length
35
+ at = end + 1
36
+ }
37
+ return at + position.character
38
+ }
39
+
23
40
  /** abide's own finding at `offset` into `text`, or at line 0 where the text was not handed in. */
24
41
  export function diagnosisAt(
25
42
  path: string,
@@ -9,6 +9,7 @@
9
9
  // layout whose segment refused it would be wrapped by a layout no address of its own reaches.
10
10
 
11
11
  import { parseRoutePattern } from '#shared/routePattern.ts'
12
+
12
13
  import { matcherOf } from './paramFile.ts'
13
14
  import type { Refusals } from './Refusals.ts'
14
15
 
@@ -16,7 +17,12 @@ import type { Refusals } from './Refusals.ts'
16
17
  export type MountedTree = { directory: string; at: string; params: string[] }
17
18
 
18
19
  /** A file with a route: its path, the name refusals give it, and the position it answers by. */
19
- type Routed = { path: string; relative: string; position: string; route: string }
20
+ type Routed = {
21
+ path: string
22
+ relative: string
23
+ position: string
24
+ route: string
25
+ }
20
26
 
21
27
  /**
22
28
  * Each routed file's matchers, by its path: every name its route's segments give, to the anchored
@@ -39,7 +45,10 @@ export async function matchersOf(
39
45
  for (const tree of trees) all.push(...tree.params)
40
46
  for (const path of all) {
41
47
  if (sources.has(path)) continue
42
- const matcher = matcherOf(labelOf(root, path), await Bun.file(path).text())
48
+ const matcher = matcherOf(
49
+ labelOf(root, path),
50
+ await Bun.file(path).text(),
51
+ )
43
52
  if (matcher instanceof Error) {
44
53
  refusals.at(path, matcher)
45
54
  sources.set(path, undefined)
@@ -79,7 +88,8 @@ export async function matchersOf(
79
88
  continue
80
89
  }
81
90
  const other = atPosition.get(shape)
82
- if (other === undefined) atPosition.set(shape, { found, file: file.relative })
91
+ if (other === undefined)
92
+ atPosition.set(shape, { found, file: file.relative })
83
93
  else if (other.found !== found)
84
94
  refusals.at(
85
95
  file.path,
@@ -10,8 +10,10 @@
10
10
 
11
11
  import { realpathSync, statSync } from 'node:fs' // bun exposes no sync stat or realpath of its own
12
12
  import { dirname, relative, resolve } from 'node:path' // bun exposes no path helpers of its own
13
+
13
14
  import { appFiles } from '#server/appTree.ts'
14
15
  import { placeOf } from '#server/placement.ts'
16
+
15
17
  import { nearest } from './nearest.ts'
16
18
  import { type AbideFile, scriptBodies } from './parseAbide.ts'
17
19
  import { Refusals } from './Refusals.ts'
@@ -9,6 +9,7 @@
9
9
  // and still never admit `.` or `..`, which the URL parser collapses into another route.
10
10
 
11
11
  import { SyntaxKind } from 'typescript/unstable/ast'
12
+
12
13
  import { tokenize } from './tokenize.ts'
13
14
 
14
15
  /** The name a param file declares: its file name before `.param.ts`. */
@@ -1,16 +1,18 @@
1
1
  import { SyntaxKind } from 'typescript/unstable/ast'
2
+
2
3
  import { URL_ATTRIBUTES } from '#shared/URL_ATTRIBUTES.ts'
4
+
3
5
  import { isComponentTag, isSlot, propKeyOf } from './classifyTemplate.ts'
4
6
  import { decodeCharacterReferences } from './decodeCharacterReferences.ts'
5
7
  import { isNamedAlone } from './isNamedAlone.ts'
6
8
  import { lineOf } from './lineOf.ts'
7
9
  import { PositionedRefusal } from './PositionedRefusal.ts'
8
- import { TEMPLATE_BLOCKS } from './TEMPLATE_BLOCKS.ts'
9
10
  import { importedNames } from './splitImports.ts'
11
+ import { TEMPLATE_BLOCKS } from './TEMPLATE_BLOCKS.ts'
10
12
  import {
11
13
  type Attribute,
12
- type Branch,
13
14
  bodiesOf,
15
+ type Branch,
14
16
  type ElementNode,
15
17
  type ForBlock,
16
18
  type InlineComponent,
@@ -106,6 +108,8 @@ type Origins = {
106
108
  names: Map<object, number>
107
109
  /** 38.116 — per element closed by a tag of its own, where that close tag's name starts. */
108
110
  closes: Map<object, number>
111
+ /** Where each block body's `<script>` setup starts — 32.48, 14.8. */
112
+ setups: number[]
109
113
  }
110
114
 
111
115
  /**
@@ -216,6 +220,7 @@ export function parseAbide(source: string): AbideFile {
216
220
  bindings: new Map(),
217
221
  names: new Map(),
218
222
  closes: new Map(),
223
+ setups: [],
219
224
  }
220
225
  const root: TemplateNode[] = []
221
226
  // The open elements, innermost last. `root` is the floor and is never popped.
@@ -365,8 +370,7 @@ export function parseAbide(source: string): AbideFile {
365
370
  if (block === styles && blocks.length > 0) {
366
371
  const owner = bodyOf(
367
372
  blocks[blocks.length - 1] as
368
- | TemplateNode
369
- | InlineComponent,
373
+ TemplateNode | InlineComponent,
370
374
  )
371
375
  owner.style =
372
376
  owner.style === null
@@ -377,8 +381,7 @@ export function parseAbide(source: string): AbideFile {
377
381
  }
378
382
  if (block === scripts && blocks.length > 0) {
379
383
  const holder = blocks[blocks.length - 1] as
380
- | TemplateNode
381
- | InlineComponent
384
+ TemplateNode | InlineComponent
382
385
  const owner = bodyOf(holder)
383
386
  if (owner.setup !== null)
384
387
  throw new Error(
@@ -388,6 +391,7 @@ export function parseAbide(source: string): AbideFile {
388
391
  )
389
392
  owner.setup = { text: raw.body }
390
393
  origins.holders.set(owner.setup, tag.end)
394
+ origins.setups.push(tag.end)
391
395
  at = raw.end
392
396
  continue
393
397
  }
@@ -1072,7 +1076,7 @@ function rawBody(
1072
1076
  * already `trim()`s for this, which is one question with two answers until now — so the end is
1073
1077
  * handed back rather than recomputed from the tag's length.
1074
1078
  */
1075
- function closeTagAt(
1079
+ export function closeTagAt(
1076
1080
  source: string,
1077
1081
  tag: string,
1078
1082
  from: number,
@@ -1113,6 +1117,9 @@ function pushText(
1113
1117
  return next
1114
1118
  }
1115
1119
 
1120
+ /** The ` if` after `{:else` — a whole word, so `{:elseif}` and `{:else ifValid}` are not one. */
1121
+ export const ELSE_IF = /^\s+if(?=[\s(])/
1122
+
1116
1123
  /**
1117
1124
  * Where the condition on a `{:else …}` arm starts in `inner`, or its end for the bare `{:else}`
1118
1125
  * that always takes.
@@ -1124,7 +1131,7 @@ function pushText(
1124
1131
  function elseSubjectAt(inner: string): number {
1125
1132
  const rest = inner.slice(':else'.length)
1126
1133
  if (rest.trim() === '') return inner.length
1127
- const keyword = /^\s+if(?=[\s(])/.exec(rest)
1134
+ const keyword = ELSE_IF.exec(rest)
1128
1135
  if (keyword === null || rest.slice(keyword[0].length).trim() === '')
1129
1136
  throw new Error(
1130
1137
  `abide: {${inner}} is not a control block. Write {:else} or {:else if <condition>}.`,
@@ -1147,28 +1154,18 @@ function trimmed(
1147
1154
  }
1148
1155
 
1149
1156
  /**
1150
- * Parses a `{#for}` header into its item, index, subject and key.
1151
- *
1152
- * `item, index of list by key` — `index` and `by` are optional, and everything after `of` up to
1153
- * a top-level `by` is the subject. Split on the KEYWORDS rather than on commas, because the
1154
- * subject may hold both.
1157
+ * The `of`, the `,` and the `by` that divide a `{#for}` header — each the first at depth zero.
1155
1158
  *
1156
- * THE SEPARATOR IS THE ONE AT DEPTH ZERO AND OUTSIDE A STRING — all three of them, and each was
1157
- * wrong its own way. `{#for { id, name } of rows}` split on the pattern's own comma and the item
1158
- * became `{ id`, which the emitters write straight into a parameter list. `{#for x of
1159
- * names.filter(n => n !== "run by me")}` split on the ` by ` inside the string literal and the
1160
- * subject ended mid-quote. One scanner for all three rather than one fix for the comma.
1159
+ * THE HEADER IS TOKENS, read by the scanner a slot is: an `of`, a `,` or a `by` inside a string,
1160
+ * a call or an object pattern is the expression's own, and only one at depth zero divides the
1161
+ * header. `by` is a word rather than a keyword, so a member named `by` is not one.
1161
1162
  */
1162
- function forBlock(
1163
- file: string,
1164
- header: { text: string; at: number },
1165
- origins: Origins,
1166
- ): ForBlock {
1167
- const source = header.text
1168
- // THE HEADER IS TOKENS, read by the scanner a slot is: an `of`, a `,` or a `by` inside a string,
1169
- // a call or an object pattern is the expression's own, and only one at depth zero divides the
1170
- // header. `by` is a word rather than a keyword, so a member named `by` is not one.
1171
- const tokens = tokenize(source)
1163
+ export function forSeparators(header: string): {
1164
+ of: Token | undefined
1165
+ comma: Token | undefined
1166
+ by: Token | undefined
1167
+ } {
1168
+ const tokens = tokenize(header)
1172
1169
  let depth = 0
1173
1170
  let of: Token | undefined
1174
1171
  let comma: Token | undefined
@@ -1191,6 +1188,29 @@ function forBlock(
1191
1188
  )
1192
1189
  by = token
1193
1190
  }
1191
+ return { of, comma, by }
1192
+ }
1193
+
1194
+ /**
1195
+ * Parses a `{#for}` header into its item, index, subject and key.
1196
+ *
1197
+ * `item, index of list by key` — `index` and `by` are optional, and everything after `of` up to
1198
+ * a top-level `by` is the subject. Split on the KEYWORDS rather than on commas, because the
1199
+ * subject may hold both.
1200
+ *
1201
+ * THE SEPARATOR IS THE ONE AT DEPTH ZERO AND OUTSIDE A STRING — all three of them, and each was
1202
+ * wrong its own way. `{#for { id, name } of rows}` split on the pattern's own comma and the item
1203
+ * became `{ id`, which the emitters write straight into a parameter list. `{#for x of
1204
+ * names.filter(n => n !== "run by me")}` split on the ` by ` inside the string literal and the
1205
+ * subject ended mid-quote. One scanner for all three rather than one fix for the comma.
1206
+ */
1207
+ function forBlock(
1208
+ file: string,
1209
+ header: { text: string; at: number },
1210
+ origins: Origins,
1211
+ ): ForBlock {
1212
+ const source = header.text
1213
+ const { of, comma, by } = forSeparators(source)
1194
1214
  // A `{#for}` with nothing to iterate rendered as a block over the subject '' — zero rows,
1195
1215
  // forever, with nothing saying why (32.22 is an ABSENT subject, not a missing one).
1196
1216
  if (of === undefined)
@@ -1,14 +1,16 @@
1
1
  import { SyntaxKind } from 'typescript/unstable/ast'
2
+
2
3
  import { PRIVATE_EXPORT } from '#server/PRIVATE_EXPORT.ts'
3
4
  import { isEventName } from '#shared/isEventName.ts'
5
+
4
6
  import { isComponentTag, propKeyOf } from './classifyTemplate.ts'
5
7
  import { isNamedAlone } from './isNamedAlone.ts'
8
+ import type { AbideFile } from './parseAbide.ts'
6
9
  import {
7
10
  type Position,
8
11
  type PositionKind,
9
12
  PositionTable,
10
13
  } from './PositionTable.ts'
11
- import type { AbideFile } from './parseAbide.ts'
12
14
  import {
13
15
  attributedImports,
14
16
  IMPORT_ATTRIBUTES,
@@ -501,12 +503,17 @@ function project(file: AbideFile): Projection {
501
503
  body(node.children, node.components)
502
504
  continue
503
505
  }
504
- // 32.16 — the catch arm binds the failure, and the checker has to see the name.
506
+ // 32.16 — the catch arm binds the failure, and the checker has to see the name. Read once
507
+ // with `void`, as a `catch` binding is never reported unused. An arm that names none binds
508
+ // nothing.
505
509
  if (node.block === 'try') {
506
510
  const [tried, caught] = node.branches as [Branch, Branch]
507
511
  text += '{\n'
508
512
  inBody(tried)
509
- text += `}\n{ const ${node.error === '' ? '__failure' : node.error}: any = undefined\n`
513
+ text +=
514
+ node.error === ''
515
+ ? '}\n{\n'
516
+ : `}\n{ const ${node.error}: any = undefined; void ${node.error}\n`
510
517
  inBody(caught)
511
518
  text += '}\n'
512
519
  continue
@@ -11,6 +11,7 @@ import {
11
11
  isObjectLiteralExpression,
12
12
  isPropertyAssignment,
13
13
  } from 'typescript/unstable/ast/is'
14
+
14
15
  import type { Method } from '#shared/rpc.ts'
15
16
 
16
17
  /**
@@ -4,18 +4,19 @@ import {
4
4
  isVariableDeclaration,
5
5
  } from 'typescript/unstable/ast/is'
6
6
  import {
7
- type Symbol as CheckedSymbol,
8
7
  type Checker,
9
8
  ObjectFlags,
10
9
  type ObjectType,
11
10
  type Project,
12
11
  SignatureKind,
12
+ type Symbol as CheckedSymbol,
13
13
  type Type,
14
14
  TypeFlags,
15
15
  type TypeParameter,
16
16
  type TypeReference,
17
17
  type UnionOrIntersectionType,
18
18
  } from 'typescript/unstable/async'
19
+
19
20
  import { aliasedSymbol } from './aliasedSymbol.ts'
20
21
 
21
22
  // WHAT THE CHECKER SAYS A NAME HOLDS, asked by both of the session's jobs over one snapshot: the
@@ -26,13 +26,14 @@ import {
26
26
  isVariableStatement,
27
27
  } from 'typescript/unstable/ast/is'
28
28
  import {
29
- type Symbol as CheckedSymbol,
30
29
  type Checker,
31
30
  SignatureKind,
31
+ type Symbol as CheckedSymbol,
32
32
  SymbolFlags,
33
33
  type Type,
34
34
  TypeFlags,
35
35
  } from 'typescript/unstable/async'
36
+
36
37
  import { aliasedSymbol } from './aliasedSymbol.ts'
37
38
  import { CheckerAnswer } from './CheckerAnswer.ts'
38
39
  import {
@@ -45,13 +46,13 @@ import {
45
46
  TRANSPARENT,
46
47
  } from './classifyRead.ts'
47
48
  import type { Finding } from './Finding.ts'
49
+ import type { AbideFile } from './parseAbide.ts'
48
50
  import {
49
51
  holdsReactive,
50
52
  type Position,
51
53
  type PositionKind,
52
54
  type PositionTable,
53
55
  } from './PositionTable.ts'
54
- import type { AbideFile } from './parseAbide.ts'
55
56
  import { INLINE_PROPS, type Span } from './projectShadow.ts'
56
57
  import { REACTIVE_MEMBERS } from './REACTIVE_MEMBERS.ts'
57
58
  import { optionNamed, reactiveOptions } from './reactiveOptions.ts'
@@ -1,10 +1,10 @@
1
- import { REGION_CLOSE, REGION_OPEN, ROW_OPEN } from '#shared/ANCHORS.ts'
2
1
  import { anchorMarkup, regionMarkup } from '#shared/anchorMarkup.ts'
2
+ import { REGION_CLOSE, REGION_OPEN, ROW_OPEN } from '#shared/ANCHORS.ts'
3
3
  import { armAnchor } from '#shared/armAnchor.ts'
4
4
  import { CHILDREN } from '#shared/CHILDREN.ts'
5
5
  import { escapeQuote } from '#shared/escapeText.ts'
6
6
  import { headKeyOf } from '#shared/headKeyOf.ts'
7
- import { Cursor, type PlacedNode, placeTemplate } from './Cursor.ts'
7
+
8
8
  import {
9
9
  attributeOf,
10
10
  type ClassifiedAttribute,
@@ -15,6 +15,7 @@ import {
15
15
  isSlot,
16
16
  type MergeGroup,
17
17
  } from './classifyTemplate.ts'
18
+ import { Cursor, type PlacedNode, placeTemplate } from './Cursor.ts'
18
19
  import { invocationProps, passedName } from './invocationProps.ts'
19
20
  import { type Lowering, PROPS } from './Lowering.ts'
20
21
  import type { AbideFile } from './parseAbide.ts'
@@ -1,6 +1,8 @@
1
1
  import { existsSync, readFileSync } from 'node:fs' // bun exposes no sync existence check or read of its own
2
2
  import { dirname, posix } from 'node:path' // bun exposes no path helpers of its own
3
+
3
4
  import { resolvedFile } from '#server/abideModules.ts'
5
+
4
6
  import { nearest } from './nearest.ts'
5
7
 
6
8
  /**
@@ -0,0 +1,150 @@
1
+ import { SyntaxKind } from 'typescript/unstable/ast'
2
+
3
+ import type { AbideFile } from './parseAbide.ts'
4
+ import type { Body, InlineComponent, TemplateNode } from './TemplateNode.ts'
5
+ import { tokenize } from './tokenize.ts'
6
+
7
+ // 38.127 — WHAT `abide format` MAY CHANGE, and nothing else: the whitespace a browser collapses, a
8
+ // run's indentation where 33.17 and 33.18 read only its kind, and the code `bun fmt` printed, read as
9
+ // its tokens — a script block's in any order, since `sortImports` moves its imports. Everything else — a tag, an attribute's text, a block's shape, a line inside a template
10
+ // literal — is compared as the parser read it.
11
+
12
+ /** Whether `before` and `after` are one template, under what `abide format` may change. 38.127. */
13
+ export function sameTemplate(before: AbideFile, after: AbideFile): boolean {
14
+ return shapeOf(before) === shapeOf(after)
15
+ }
16
+
17
+ function shapeOf(file: AbideFile): string {
18
+ const parts: string[] = []
19
+ for (const code of file.modules) parts.push(blockShape(code))
20
+ for (const code of file.servers) parts.push(blockShape(code))
21
+ for (const code of file.scripts) parts.push(blockShape(code))
22
+ for (const style of file.styles) parts.push(styleShape(style))
23
+ parts.push(file.generics ?? '')
24
+ nodesShape(file.head, parts)
25
+ if (file.document !== undefined) {
26
+ nodesShape([file.document.html], parts)
27
+ nodesShape([file.document.body], parts)
28
+ }
29
+ nodesShape(file.template, parts)
30
+ for (const component of file.components) componentShape(component, parts)
31
+ return parts.join('\u0000')
32
+ }
33
+
34
+ function nodesShape(nodes: TemplateNode[], parts: string[]): void {
35
+ for (const node of nodes) {
36
+ if (node.kind === 'text') {
37
+ parts.push(`text ${textShape(node.text)}`)
38
+ continue
39
+ }
40
+ if (node.kind === 'expression') {
41
+ parts.push(`slot ${codeShape(node.source)}`)
42
+ continue
43
+ }
44
+ if (node.kind === 'element') {
45
+ parts.push(`<${node.tag}`)
46
+ for (const attribute of node.attributes)
47
+ parts.push(
48
+ attribute.expression
49
+ ? `${attribute.name}={${codeShape(attribute.value)}}`
50
+ : `${attribute.name}=${attribute.value}`,
51
+ )
52
+ nodesShape(node.children, parts)
53
+ for (const component of node.components)
54
+ componentShape(component, parts)
55
+ for (const component of node.passed)
56
+ componentShape(component, parts)
57
+ parts.push(`</${node.tag}>`)
58
+ continue
59
+ }
60
+ if (node.block === 'for') {
61
+ parts.push(
62
+ `{#for ${codeShape(node.item)},${codeShape(node.index)} of ${codeShape(node.subject)} by ${codeShape(node.key)}`,
63
+ )
64
+ bodyShape(node, parts)
65
+ } else {
66
+ parts.push(
67
+ node.block === 'try'
68
+ ? `{#try ${node.error}`
69
+ : `{#${node.block}`,
70
+ )
71
+ for (const branch of node.branches) {
72
+ parts.push(`{:${codeShape(branch.subject)}`)
73
+ bodyShape(branch, parts)
74
+ }
75
+ }
76
+ parts.push('{/}')
77
+ }
78
+ }
79
+
80
+ function bodyShape(body: Body, parts: string[]): void {
81
+ parts.push(
82
+ body.setup === null ? '' : blockShape(body.setup.text),
83
+ body.style === null ? '' : styleShape(body.style),
84
+ )
85
+ nodesShape(body.children, parts)
86
+ for (const component of body.components) componentShape(component, parts)
87
+ }
88
+
89
+ function componentShape(component: InlineComponent, parts: string[]): void {
90
+ parts.push(
91
+ `{#component ${component.name}(${component.pattern}: ${component.type})`,
92
+ )
93
+ bodyShape(component, parts)
94
+ parts.push('{/component}')
95
+ }
96
+
97
+ /**
98
+ * A text node as the browser renders it: within text, every whitespace run holding a newline is one
99
+ * space however it is indented. A node that is whitespace alone keeps its KIND — a newline alone, or
100
+ * one carrying indentation — since 33.17 and 33.18 drop the two apart.
101
+ */
102
+ function textShape(text: string): string {
103
+ if (text.trim() === '') return /[ \t]/.test(text) ? '\n ' : '\n'
104
+ return text.replace(/\s*\n\s*/g, '\n')
105
+ }
106
+
107
+ /** Code as its tokens, in order. */
108
+ function codeShape(code: string): string {
109
+ return tokensOf(code).join(' ')
110
+ }
111
+
112
+ /** A script block's code as its tokens in any order: `bun fmt`'s `sortImports` moves its imports. */
113
+ function blockShape(code: string): string {
114
+ return tokensOf(code).sort().join(' ')
115
+ }
116
+
117
+ /**
118
+ * Code's tokens, with what a printer adds or drops at will taken out: the separators `;` and `,`,
119
+ * the parentheses, and how a string or a number is spelled. A template literal's text is a token,
120
+ * so a line moved inside one is a different token.
121
+ */
122
+ function tokensOf(code: string): string[] {
123
+ const tokens: string[] = []
124
+ for (const token of tokenize(code))
125
+ switch (token.kind) {
126
+ case SyntaxKind.SemicolonToken:
127
+ case SyntaxKind.CommaToken:
128
+ case SyntaxKind.OpenParenToken:
129
+ case SyntaxKind.CloseParenToken:
130
+ continue
131
+ case SyntaxKind.StringLiteral:
132
+ tokens.push(
133
+ JSON.stringify(
134
+ token.text.slice(1, -1).replace(/\\(['"])/g, '$1'),
135
+ ),
136
+ )
137
+ continue
138
+ case SyntaxKind.NumericLiteral:
139
+ tokens.push(String(Number(token.text.replaceAll('_', ''))))
140
+ continue
141
+ default:
142
+ tokens.push(token.text)
143
+ }
144
+ return tokens
145
+ }
146
+
147
+ /** A style with its whitespace and its `;`s taken out, which CSS reads none of outside a string. */
148
+ function styleShape(style: string): string {
149
+ return style.replace(/[\s;]+/g, '')
150
+ }
@@ -5,6 +5,7 @@
5
5
  // half-made project finishes it rather than overwriting it.
6
6
 
7
7
  import { withoutTrailingSlash } from '#server/withoutTrailingSlash.ts'
8
+
8
9
  import abidePackage from '../../package.json' with { type: 'json' }
9
10
  import { COMPILER_OPTIONS } from './writeDefaultProject.ts'
10
11
 
@@ -39,7 +40,7 @@ const SOURCE_ALIAS = { '#src/*': ['./src/*', './src/*.ts'] }
39
40
  * lands first on the PATH inside `bun run`, shadowing the bun that runs everything else.
40
41
  */
41
42
  export const BUNFIG =
42
- '# 38.122 — a peer is not installed: an installed `bun` would shadow this one inside `bun run`.\n[install]\npeer = false\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'
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.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
44
 
44
45
  // WRITTEN, NOT COPIED, beside the manifests: a packer drops a `bunfig.toml` or a `.gitignore` from
45
46
  // the tarball (`bun pm pack` both, `npm pack` the second), so neither survives as a starter file.
@@ -55,6 +56,8 @@ function filesFor(name: string): Record<string, string> {
55
56
  scripts: {
56
57
  // 38.103 — the checker over `.abide`, which `tsc` does not read.
57
58
  check: 'abide check',
59
+ // 38.125 — every `.abide`, then every other file `bun fmt` reads.
60
+ format: 'abide format && bun fmt',
58
61
  dev: 'abide dev',
59
62
  build: 'abide build',
60
63
  start: 'abide start',
@@ -101,16 +104,6 @@ function filesFor(name: string): Record<string, string> {
101
104
  null,
102
105
  4,
103
106
  )}\n`,
104
- 'biome.json': `${JSON.stringify(
105
- {
106
- formatter: { indentStyle: 'space', indentWidth: 4 },
107
- javascript: {
108
- formatter: { quoteStyle: 'single', semicolons: 'asNeeded' },
109
- },
110
- },
111
- null,
112
- 4,
113
- )}\n`,
114
107
  }
115
108
  }
116
109