@abide/abide 0.117.2 → 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 (156) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/abide.ts +74 -11
  3. package/docs/REGISTRY.md +18 -15
  4. package/docs/RULEBOOK.md +71 -10
  5. package/docs/abide.md +584 -274
  6. package/package.json +1 -1
  7. package/scaffold/AGENTS.md +1 -1
  8. package/scaffold/src/getGreeting.test.ts +2 -1
  9. package/scaffold/src/index.abide +5 -2
  10. package/scaffold/src/index.test.ts +2 -0
  11. package/scaffold/src/layout.abide +6 -6
  12. package/src/compiler/Lowering.ts +20 -2
  13. package/src/compiler/PositionTable.ts +1 -0
  14. package/src/compiler/Refusals.ts +1 -0
  15. package/src/compiler/TypeSession.ts +49 -13
  16. package/src/compiler/aliasedSymbol.ts +1 -1
  17. package/src/compiler/ambientReadsIn.ts +1 -0
  18. package/src/compiler/appLayout.ts +55 -3
  19. package/src/compiler/appMap.ts +15 -0
  20. package/src/compiler/argsDefaultsIn.ts +1 -0
  21. package/src/compiler/argsSchemaOf.ts +1 -0
  22. package/src/compiler/checkApp.ts +3 -1
  23. package/src/compiler/checkedShadow.ts +2 -1
  24. package/src/compiler/classifyRead.ts +1 -0
  25. package/src/compiler/classifyTemplate.ts +1 -0
  26. package/src/compiler/compileApp.ts +5 -3
  27. package/src/compiler/compiledEntry.ts +2 -1
  28. package/src/compiler/componentGraph.ts +1 -0
  29. package/src/compiler/declaredExports.ts +4 -2
  30. package/src/compiler/emitClient.ts +19 -14
  31. package/src/compiler/emitComponent.ts +1 -0
  32. package/src/compiler/emitSource.ts +2 -1
  33. package/src/compiler/formatAbide.ts +857 -0
  34. package/src/compiler/formatDiagnosis.ts +1 -0
  35. package/src/compiler/handlersUnder.ts +7 -3
  36. package/src/compiler/hookCalls.ts +1 -0
  37. package/src/compiler/lineOf.ts +17 -0
  38. package/src/compiler/matchersOf.ts +157 -0
  39. package/src/compiler/mountsOf.ts +5 -0
  40. package/src/compiler/paramFile.ts +125 -0
  41. package/src/compiler/parseAbide.ts +48 -28
  42. package/src/compiler/projectShadow.ts +11 -4
  43. package/src/compiler/reactiveOptions.ts +1 -0
  44. package/src/compiler/reactiveTypes.ts +2 -1
  45. package/src/compiler/readsIn.ts +3 -2
  46. package/src/compiler/renderMarkup.ts +3 -2
  47. package/src/compiler/resolverIn.ts +2 -0
  48. package/src/compiler/sameTemplate.ts +150 -0
  49. package/src/compiler/scaffold.ts +4 -11
  50. package/src/compiler/serveLanguage.ts +48 -21
  51. package/src/compiler/sharedModuleWrites.ts +2 -1
  52. package/src/compiler/sourceMapOf.ts +1 -0
  53. package/src/compiler/splitImports.ts +2 -0
  54. package/src/compiler/templateWarnings.ts +2 -2
  55. package/src/compiler/trackingWarnings.ts +1 -0
  56. package/src/compiler/writeDefaultProject.ts +5 -1
  57. package/src/server/BRIDGE_VERSION.ts +1 -1
  58. package/src/server/abideModules.ts +2 -0
  59. package/src/server/answerFailure.ts +1 -0
  60. package/src/server/answerRequest.ts +57 -19
  61. package/src/server/answerRpc.ts +6 -2
  62. package/src/server/answerSocket.ts +2 -1
  63. package/src/server/answerValue.ts +1 -0
  64. package/src/server/answerWorker.ts +1 -0
  65. package/src/server/appTree.ts +18 -9
  66. package/src/server/argsFor.ts +1 -0
  67. package/src/server/assistant.ts +2 -1
  68. package/src/server/banner.ts +2 -0
  69. package/src/server/bridgeRelay.ts +1 -0
  70. package/src/server/callerOf.ts +1 -0
  71. package/src/server/claudeApi.ts +1 -0
  72. package/src/server/claudeCode.ts +1 -0
  73. package/src/server/clientBundle.ts +1 -0
  74. package/src/server/coerceArgs.ts +1 -2
  75. package/src/server/commands.ts +9 -0
  76. package/src/server/compressAnswer.ts +2 -1
  77. package/src/server/config.ts +2 -0
  78. package/src/server/createApp.ts +4 -2
  79. package/src/server/crossingOrigin.ts +1 -0
  80. package/src/server/csp.ts +1 -0
  81. package/src/server/deferredBuilds.ts +1 -0
  82. package/src/server/derivedArgs.ts +1 -0
  83. package/src/server/devWorker.ts +2 -1
  84. package/src/server/faceOf.ts +2 -7
  85. package/src/server/generatedSurfaces.ts +79 -7
  86. package/src/server/hooks.ts +1 -0
  87. package/src/server/iconAccent.ts +1 -1
  88. package/src/server/installServer.ts +1 -0
  89. package/src/server/installSourceMaps.ts +1 -0
  90. package/src/server/launcherPage.ts +2 -2
  91. package/src/server/manifestOf.ts +1 -0
  92. package/src/server/muxUpgrade.ts +1 -0
  93. package/src/server/nativeMenu.ts +2 -0
  94. package/src/server/placement.ts +12 -2
  95. package/src/server/principal.ts +2 -1
  96. package/src/server/remote.ts +7 -2
  97. package/src/server/render.ts +3 -3
  98. package/src/server/renderComponent.ts +1 -0
  99. package/src/server/reportHealth.ts +1 -0
  100. package/src/server/requestScope.ts +3 -1
  101. package/src/server/runDesktop.ts +3 -2
  102. package/src/server/seedBuffer.ts +1 -0
  103. package/src/server/serveDev.ts +3 -2
  104. package/src/server/shell.ts +2 -0
  105. package/src/server/streamEvent.ts +1 -0
  106. package/src/server/surfaceOf.ts +6 -1
  107. package/src/server/testBuild.ts +1 -0
  108. package/src/server/testMount.ts +1 -0
  109. package/src/server/trace.ts +2 -0
  110. package/src/server/withAbideSources.ts +1 -0
  111. package/src/shared/Attributes.ts +12 -8
  112. package/src/shared/cookies.ts +1 -1
  113. package/src/shared/escapeText.ts +1 -1
  114. package/src/shared/filledPath.ts +16 -10
  115. package/src/shared/index.ts +1 -5
  116. package/src/shared/jsonSchema.ts +1 -3
  117. package/src/shared/matchRoute.ts +16 -4
  118. package/src/shared/menuBridge.ts +1 -2
  119. package/src/shared/outbox.ts +1 -1
  120. package/src/shared/reactive/Keyed.ts +2 -2
  121. package/src/shared/reactive/Reactive.ts +12 -16
  122. package/src/shared/reactive/index.ts +3 -4
  123. package/src/shared/reactive/isReactive.ts +1 -1
  124. package/src/shared/reactive/scoped.ts +2 -2
  125. package/src/shared/reactive/share.ts +1 -1
  126. package/src/shared/reactive/watch.ts +4 -4
  127. package/src/shared/refuse.ts +1 -1
  128. package/src/shared/routePattern.ts +66 -14
  129. package/src/shared/rpc.ts +8 -11
  130. package/src/shared/url.ts +24 -7
  131. package/src/shared/validateJson.ts +1 -1
  132. package/src/ui/ArmRegion.ts +1 -0
  133. package/src/ui/Level.ts +1 -0
  134. package/src/ui/List.ts +1 -0
  135. package/src/ui/RawRun.ts +1 -0
  136. package/src/ui/Router.ts +1 -0
  137. package/src/ui/TryRegion.ts +1 -0
  138. package/src/ui/adoptWalk.ts +2 -1
  139. package/src/ui/bindElement.ts +1 -0
  140. package/src/ui/bindFiles.ts +1 -0
  141. package/src/ui/bindGroup.ts +1 -0
  142. package/src/ui/bindProperty.ts +1 -0
  143. package/src/ui/bindSpread.ts +1 -0
  144. package/src/ui/index.ts +1 -6
  145. package/src/ui/outboxQueue.ts +1 -1
  146. package/src/ui/parseMarkup.ts +2 -1
  147. package/src/ui/reconcile.ts +2 -1
  148. package/src/ui/reconcileHead.ts +1 -0
  149. package/src/ui/regions.ts +2 -1
  150. package/src/ui/registerWorker.ts +1 -0
  151. package/src/ui/rows.ts +1 -0
  152. package/src/ui/setMarkup.ts +1 -0
  153. package/src/ui/setText.ts +1 -0
  154. package/src/ui/socketWire.ts +4 -3
  155. package/src/ui/wire.ts +2 -1
  156. /package/scaffold/src/{getGreeting.ts → getGreeting.handler.ts} +0 -0
@@ -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
 
@@ -45,7 +46,7 @@ export async function handlersUnder(
45
46
  sources: readonly Source[],
46
47
  files: readonly Pick<
47
48
  PlacedHandler,
48
- 'path' | 'relative' | 'route' | 'scope'
49
+ 'path' | 'relative' | 'route' | 'scope' | 'matchers'
49
50
  >[],
50
51
  refusals: Refusals,
51
52
  ): Promise<{ mounts: Mount[]; warnings: Diagnosis[] }> {
@@ -84,6 +85,7 @@ export async function handlersUnder(
84
85
  relative,
85
86
  route: address,
86
87
  scope,
88
+ matchers,
87
89
  } = files[at] as (typeof files)[number]
88
90
  const exported = declared[at]
89
91
  if (exported === undefined) continue
@@ -99,7 +101,7 @@ export async function handlersUnder(
99
101
  )
100
102
  if (handler?.method === undefined)
101
103
  throw new Error(
102
- `abide: ${relative} is a handler file — a .ts directly in an address directory — and exports no handler as its default. Export one (\`export default GET(…)\`), or move the file into a lib directory. RULEBOOK 16.73, 23.22.`,
104
+ `abide: ${relative} is a handler file — a <name>.handler.ts directly in an address directory — and exports no handler as its default. Export one (\`export default GET(…)\`), or move the file into a lib directory. RULEBOOK 16.73, 23.22.`,
103
105
  )
104
106
  const framework = frameworkRouteRefusal(address, relative)
105
107
  if (framework !== undefined) throw framework
@@ -119,6 +121,7 @@ export async function handlersUnder(
119
121
  name: address.slice(address.lastIndexOf('/') + 1),
120
122
  export: 'default',
121
123
  scope,
124
+ matchers,
122
125
  private: false,
123
126
  ui: handler.ui,
124
127
  cli: handler.cli,
@@ -150,6 +153,7 @@ export async function handlersUnder(
150
153
  name,
151
154
  export: each.name,
152
155
  scope,
156
+ matchers: {},
153
157
  private: true,
154
158
  ui: each.ui,
155
159
  cli: false,
@@ -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,
@@ -0,0 +1,157 @@
1
+ // 23.26, 23.27 — WHICH MATCHER EACH SEGMENT NAMES, resolved once at build and carried into the route
2
+ // tables as text, so nothing at run time looks a name up. A segment's `=NAME` is the param file
3
+ // `NAME.param.ts` in the directory holding that segment or the nearest one above it, below the root
4
+ // the file is filed under: the app's root for the app's own, and a mounted directory for the files
5
+ // under it (23.19), so a host's matcher never decides a mounted package's segment.
6
+ //
7
+ // ONE MATCHER PER POSITION across every file filed there (23.27): a layout, a page and a handler
8
+ // directory at `/holdings/[symbol=TICKER]` share the segment, and a page that took `BRK.B` under a
9
+ // layout whose segment refused it would be wrapped by a layout no address of its own reaches.
10
+
11
+ import { parseRoutePattern } from '#shared/routePattern.ts'
12
+
13
+ import { matcherOf } from './paramFile.ts'
14
+ import type { Refusals } from './Refusals.ts'
15
+
16
+ /** A directory a file mounts (23.19), where it mounts, and the param files under it, by path. */
17
+ export type MountedTree = { directory: string; at: string; params: string[] }
18
+
19
+ /** A file with a route: its path, the name refusals give it, and the position it answers by. */
20
+ type Routed = {
21
+ path: string
22
+ relative: string
23
+ position: string
24
+ route: string
25
+ }
26
+
27
+ /**
28
+ * Each routed file's matchers, by its path: every name its route's segments give, to the anchored
29
+ * source of the param file it resolves to — empty where its route names none.
30
+ *
31
+ * `params` is every param file of the app's own, by path. Refusals land at the file the fault is in:
32
+ * a param file that declares no matcher at itself, a name that resolves to none at the segment's file.
33
+ */
34
+ export async function matchersOf(
35
+ root: string,
36
+ routed: readonly Routed[],
37
+ params: readonly string[],
38
+ trees: readonly MountedTree[],
39
+ refusals: Refusals,
40
+ ): Promise<Map<string, Record<string, string>>> {
41
+ // Every param file's matcher, read once whichever routes name it; `undefined` where 23.24 or
42
+ // 23.25 refused the file, so a segment naming it refuses nothing more.
43
+ const sources = new Map<string, string | undefined>()
44
+ const all = [...params]
45
+ for (const tree of trees) all.push(...tree.params)
46
+ for (const path of all) {
47
+ if (sources.has(path)) continue
48
+ const matcher = matcherOf(
49
+ labelOf(root, path),
50
+ await Bun.file(path).text(),
51
+ )
52
+ if (matcher instanceof Error) {
53
+ refusals.at(path, matcher)
54
+ sources.set(path, undefined)
55
+ } else sources.set(path, matcher)
56
+ }
57
+ const byPath = new Map<string, Record<string, string>>()
58
+ // 23.27 — what each parameter position resolved to, and the first file that said so.
59
+ const atPosition = new Map<string, { found: string; file: string }>()
60
+ for (const file of routed) {
61
+ const matchers: Record<string, string> = {}
62
+ const tree = treeOf(file.path, trees)
63
+ const positionParts = file.position.split('/')
64
+ let shape = ''
65
+ let at = 0
66
+ for (const segment of parseRoutePattern(file.route)) {
67
+ shape += `/${segment.kind === 'static' ? segment.name : `[${segment.kind}]`}`
68
+ const index = at
69
+ at += 1
70
+ if (segment.kind === 'static') continue
71
+ const found =
72
+ segment.matcher === undefined
73
+ ? ''
74
+ : nearestParam(
75
+ root,
76
+ tree,
77
+ positionParts.slice(0, index),
78
+ segment.matcher,
79
+ sources,
80
+ )
81
+ if (found === undefined) {
82
+ refusals.at(
83
+ file.path,
84
+ new Error(
85
+ `abide: ${file.relative} names the matcher ${segment.matcher}, and no ${segment.matcher}.param.ts is in the directory holding that segment or any directory above it. RULEBOOK 23.26.`,
86
+ ),
87
+ )
88
+ continue
89
+ }
90
+ const other = atPosition.get(shape)
91
+ if (other === undefined)
92
+ atPosition.set(shape, { found, file: file.relative })
93
+ else if (other.found !== found)
94
+ refusals.at(
95
+ file.path,
96
+ new Error(
97
+ `abide: ${other.file} and ${file.relative} decide the parameter at ${shape} by ${describe(root, other.found)} and by ${describe(root, found)}, and one position takes one matcher. RULEBOOK 23.27.`,
98
+ ),
99
+ )
100
+ const source = found === '' ? undefined : sources.get(found)
101
+ if (segment.matcher !== undefined && source !== undefined)
102
+ matchers[segment.matcher] = source
103
+ }
104
+ byPath.set(file.path, matchers)
105
+ }
106
+ return byPath
107
+ }
108
+
109
+ /**
110
+ * The param file `NAME.param.ts` a segment at `holding` names, nearest first, or `undefined`.
111
+ *
112
+ * `holding` is the position of the directory holding the segment, and the walk stops at the root
113
+ * the segment is filed under: inside a mounted directory, that directory; a segment of the position
114
+ * the mount itself takes is the mounting app's.
115
+ */
116
+ function nearestParam(
117
+ root: string,
118
+ tree: MountedTree | undefined,
119
+ holding: string[],
120
+ name: string,
121
+ sources: Map<string, string | undefined>,
122
+ ): string | undefined {
123
+ const atParts = tree === undefined ? 0 : tree.at.split('/').length - 1
124
+ const inside = tree !== undefined && holding.length >= atParts
125
+ const base = inside ? tree.directory : root
126
+ const below = inside ? holding.slice(atParts) : holding
127
+ for (let depth = below.length; depth >= 0; depth -= 1) {
128
+ const directory = [base, ...below.slice(0, depth)].join('/')
129
+ const candidate = `${directory}/${name}.param.ts`
130
+ if (sources.has(candidate)) return candidate
131
+ }
132
+ return undefined
133
+ }
134
+
135
+ /** The mounted directory `path` lies in, the deepest where two nest, or `undefined` for the app's own. */
136
+ function treeOf(
137
+ path: string,
138
+ trees: readonly MountedTree[],
139
+ ): MountedTree | undefined {
140
+ let deepest: MountedTree | undefined
141
+ for (const tree of trees)
142
+ if (
143
+ path.startsWith(`${tree.directory}/`) &&
144
+ (deepest === undefined ||
145
+ tree.directory.length > deepest.directory.length)
146
+ )
147
+ deepest = tree
148
+ return deepest
149
+ }
150
+
151
+ function labelOf(root: string, path: string): string {
152
+ return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path
153
+ }
154
+
155
+ function describe(root: string, found: string): string {
156
+ return found === '' ? 'no matcher' : labelOf(root, found)
157
+ }
@@ -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'
@@ -31,6 +33,8 @@ type DirectoryMount = {
31
33
  at: string
32
34
  files: Placed[]
33
35
  handlers: Placed[]
36
+ /** 23.26 — the param files under it, by path: what its own segments' matchers resolve in. */
37
+ params: string[]
34
38
  }
35
39
 
36
40
  /**
@@ -85,6 +89,7 @@ export function mountsOf(
85
89
  at,
86
90
  files: found.abide.map(placed),
87
91
  handlers: found.handlers.map(placed),
92
+ params: found.params.map((below) => `${directory}/${below}`),
88
93
  })
89
94
  }
90
95
  return mounts
@@ -0,0 +1,125 @@
1
+ // 23.24, 23.25 — A PARAM FILE, READ AND NEVER RUN. `<NAME>.param.ts` says what a segment naming
2
+ // `NAME` accepts, and the build carries that into both route tables as a regex literal, so the
3
+ // file is read off its tokens: one `export const NAME = /…/`, nothing else. A file that does
4
+ // anything more could only be honoured by running it, and nothing about building a route table is
5
+ // a reason to run app code.
6
+ //
7
+ // 30.36 — THE DOT-SEGMENT FLOOR IS WRITTEN INTO THE SOURCE rather than tested beside it, so the walk
8
+ // holds one test per part whatever the segment names: a matcher can lift the period default (30.34)
9
+ // and still never admit `.` or `..`, which the URL parser collapses into another route.
10
+
11
+ import { SyntaxKind } from 'typescript/unstable/ast'
12
+
13
+ import { tokenize } from './tokenize.ts'
14
+
15
+ /** The name a param file declares: its file name before `.param.ts`. */
16
+ export function paramNameOf(path: string): string {
17
+ return path.slice(path.lastIndexOf('/') + 1, -'.param.ts'.length)
18
+ }
19
+
20
+ /**
21
+ * The anchored source of the matcher the param file at `relative` declares, or why it declares none.
22
+ *
23
+ * Anchored as `^(?!\.\.?$)(?:…)$`: the whole part, percent-decoded, and never a dot-segment.
24
+ */
25
+ export function matcherOf(relative: string, text: string): string | Error {
26
+ const name = paramNameOf(relative)
27
+ const tokens = tokenize(text)
28
+ const kinds: SyntaxKind[] = []
29
+ for (const token of tokens)
30
+ if (token.kind !== SyntaxKind.EndOfFile) kinds.push(token.kind)
31
+ const declared =
32
+ kinds.length >= 5 &&
33
+ kinds.length <= 6 &&
34
+ kinds[0] === SyntaxKind.ExportKeyword &&
35
+ kinds[1] === SyntaxKind.ConstKeyword &&
36
+ kinds[2] === SyntaxKind.Identifier &&
37
+ kinds[3] === SyntaxKind.EqualsToken &&
38
+ kinds[4] === SyntaxKind.RegularExpressionLiteral &&
39
+ (kinds.length === 5 || kinds[5] === SyntaxKind.SemicolonToken)
40
+ if (!declared)
41
+ return new Error(
42
+ `abide: ${relative} must hold one \`export const ${name} = /…/\` and nothing else, an import included. RULEBOOK 23.24.`,
43
+ )
44
+ const exported = (tokens[2] as { text: string }).text
45
+ if (exported !== name)
46
+ return new Error(
47
+ `abide: ${relative} exports ${exported}, and a param file exports the name it is filed under, ${name}. RULEBOOK 23.24.`,
48
+ )
49
+ const literal = (tokens[4] as { text: string }).text
50
+ const close = literal.lastIndexOf('/')
51
+ if (close !== literal.length - 1)
52
+ return new Error(
53
+ `abide: ${relative} carries the flag ${literal.slice(close + 1)}, and a matcher carries none: a route, OpenAPI and a JSON Schema's \`pattern\` would each read it differently. RULEBOOK 23.24.`,
54
+ )
55
+ const body = literal.slice(1, close)
56
+ const runaway = runawayIn(body)
57
+ if (runaway !== undefined)
58
+ return new Error(
59
+ `abide: ${relative} carries ${runaway}, which can take time exponential in a part's length on an address made to. RULEBOOK 23.25.`,
60
+ )
61
+ return `^(?!\\.\\.?$)(?:${body})$`
62
+ }
63
+
64
+ /**
65
+ * 23.25 — what in a regex's source can backtrack without bound, or `undefined`.
66
+ *
67
+ * A repetition with no upper bound (`*`, `+`, `{n,}`) of a group that itself holds one, and a
68
+ * backreference. Read character by character rather than parsed: a class and an escape are skipped
69
+ * as single atoms, which is all the shape the two questions need.
70
+ */
71
+ export function runawayIn(body: string): string | undefined {
72
+ // One entry per open group: whether it holds an unbounded repetition.
73
+ const holds: boolean[] = [false]
74
+ // Whether the atom just read is a group holding one.
75
+ let groupHolding = false
76
+ for (let at = 0; at < body.length; at += 1) {
77
+ const char = body[at] as string
78
+ if (char === '\\') {
79
+ const next = body[at + 1] ?? ''
80
+ if ((next >= '1' && next <= '9') || next === 'k')
81
+ return 'a backreference'
82
+ at += 1
83
+ groupHolding = false
84
+ continue
85
+ }
86
+ if (char === '[') {
87
+ at += 1
88
+ while (at < body.length && body[at] !== ']')
89
+ at += body[at] === '\\' ? 2 : 1
90
+ groupHolding = false
91
+ continue
92
+ }
93
+ if (char === '(') {
94
+ holds.push(false)
95
+ groupHolding = false
96
+ continue
97
+ }
98
+ if (char === ')') {
99
+ const inner = holds.pop() ?? false
100
+ if (inner) holds[holds.length - 1] = true
101
+ groupHolding = inner
102
+ continue
103
+ }
104
+ const unbounded =
105
+ char === '*' ||
106
+ char === '+' ||
107
+ (char === '{' && /^\{\d+,\}/.test(body.slice(at)))
108
+ if (unbounded) {
109
+ if (groupHolding) return 'a repetition of a group that repeats'
110
+ holds[holds.length - 1] = true
111
+ groupHolding = false
112
+ continue
113
+ }
114
+ // A bounded `{n}` or `{n,m}` repeats a group a fixed number of times, and keeps what it
115
+ // repeats as the atom a following quantifier would apply to.
116
+ if (char === '{') {
117
+ const end = body.indexOf('}', at)
118
+ if (end !== -1) at = end
119
+ continue
120
+ }
121
+ if (char === '?') continue
122
+ groupHolding = false
123
+ }
124
+ return undefined
125
+ }
@@ -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
@@ -763,7 +770,7 @@ function refuseExport(source: string): void {
763
770
  if (moved !== 0) depth += moved
764
771
  else if (depth === 0 && text === EXPORT && previous !== '.')
765
772
  throw new Error(
766
- 'abide: a .abide cannot export — its component is its one export. A handler another file calls is a handler file (a .ts beside the page, `export default GET(…)`); a value files share is a module under a lib directory. RULEBOOK 34.17.',
773
+ 'abide: a .abide cannot export — its component is its one export. A handler another file calls is a handler file (a <name>.handler.ts beside the page, `export default GET(…)`); a value files share is a module under a lib directory. RULEBOOK 34.17.',
767
774
  )
768
775
  previous = text
769
776
  }
@@ -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'