@abide/abide 0.104.0 → 0.105.1

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 (85) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/abide.ts +80 -173
  3. package/docs/REGISTRY.md +17 -17
  4. package/docs/RULEBOOK.md +50 -12
  5. package/docs/abide.md +9 -9
  6. package/package.json +1 -1
  7. package/src/compiler/Lowering.ts +15 -5
  8. package/src/compiler/Refusals.ts +57 -0
  9. package/src/compiler/TypeSession.ts +45 -86
  10. package/src/compiler/appLayout.ts +447 -0
  11. package/src/compiler/appMap.ts +21 -20
  12. package/src/compiler/checkApp.ts +61 -97
  13. package/src/compiler/checkedShadow.ts +11 -9
  14. package/src/compiler/classifyRead.ts +3 -6
  15. package/src/compiler/compileApp.ts +180 -512
  16. package/src/compiler/componentGraph.ts +2 -71
  17. package/src/compiler/declaredExports.ts +79 -60
  18. package/src/compiler/emitClient.ts +11 -13
  19. package/src/compiler/emitComponent.ts +21 -6
  20. package/src/compiler/emitSource.ts +20 -20
  21. package/src/compiler/handlersUnder.ts +46 -36
  22. package/src/compiler/hookCalls.ts +152 -0
  23. package/src/compiler/mountsOf.ts +31 -54
  24. package/src/compiler/readSources.ts +27 -42
  25. package/src/compiler/readsIn.ts +5 -16
  26. package/src/compiler/resolverIn.ts +77 -0
  27. package/src/server/abideModules.ts +92 -123
  28. package/src/server/answerRequest.ts +18 -4
  29. package/src/server/answerSocket.ts +136 -76
  30. package/src/server/answerValue.ts +28 -15
  31. package/src/server/appTree.ts +49 -71
  32. package/src/server/claudeApi.ts +2 -5
  33. package/src/server/clientBundle.ts +198 -0
  34. package/src/server/coerceArgs.ts +1 -1
  35. package/src/server/commands.ts +93 -57
  36. package/src/server/createApp.ts +90 -16
  37. package/src/server/deferredBuilds.ts +175 -0
  38. package/src/server/devWorker.ts +107 -319
  39. package/src/server/faceOf.ts +120 -0
  40. package/src/server/fenceServer.ts +10 -18
  41. package/src/server/index.ts +3 -0
  42. package/src/server/placement.ts +167 -0
  43. package/src/server/remote.ts +14 -42
  44. package/src/server/render.ts +25 -17
  45. package/src/server/renderComponent.ts +9 -5
  46. package/src/server/requestScope.ts +10 -4
  47. package/src/server/responseRelay.ts +389 -0
  48. package/src/server/runCompiled.ts +9 -12
  49. package/src/server/runDesktop.ts +6 -9
  50. package/src/server/serveBuild.ts +26 -73
  51. package/src/server/serveDev.ts +19 -176
  52. package/src/server/streamEvent.ts +29 -0
  53. package/src/server/withAbideSources.ts +2 -1
  54. package/src/shared/HandlerRoute.ts +68 -0
  55. package/src/shared/REFUSAL_KEY.ts +5 -0
  56. package/src/shared/answerOf.ts +105 -78
  57. package/src/shared/filledPath.ts +1 -9
  58. package/src/shared/framing.ts +127 -128
  59. package/src/shared/queryText.ts +15 -7
  60. package/src/shared/reactive/Cursor.ts +4 -2
  61. package/src/shared/reactive/Reactive.ts +285 -197
  62. package/src/shared/reactive/index.ts +11 -8
  63. package/src/shared/reactive/pairReactive.ts +1 -1
  64. package/src/shared/reactive/scoped.ts +4 -3
  65. package/src/shared/reactive/share.ts +11 -1
  66. package/src/shared/routePattern.ts +0 -123
  67. package/src/shared/rpc.ts +17 -24
  68. package/src/shared/rpcRequest.ts +18 -47
  69. package/src/shared/takeEarlyCall.ts +12 -37
  70. package/src/shared/url.ts +1 -1
  71. package/src/ui/Level.ts +276 -0
  72. package/src/ui/List.ts +17 -17
  73. package/src/ui/Router.ts +20 -87
  74. package/src/ui/applyHead.ts +92 -79
  75. package/src/ui/index.ts +4 -2
  76. package/src/ui/leave.ts +5 -0
  77. package/src/ui/reconcile.ts +23 -104
  78. package/src/ui/regions.ts +27 -2
  79. package/src/ui/rows.ts +123 -2
  80. package/src/ui/wire.ts +12 -27
  81. package/src/compiler/hooksCalled.ts +0 -35
  82. package/src/server/clientBuildOptions.ts +0 -54
  83. package/src/server/refuseAddressCollision.ts +0 -37
  84. package/src/shared/reactive/chunksOfStream.ts +0 -33
  85. package/src/ui/MOUNTING_LEVEL.ts +0 -18
@@ -1,23 +1,11 @@
1
1
  import { relative } from 'node:path' // bun exposes no path helpers of its own
2
- import { appFiles } from '#server/appTree.ts'
3
- import { placeOf } from '#shared/routePattern.ts'
4
- import {
5
- type AppChecker,
6
- emitApp,
7
- refuseModuleHooks,
8
- resolverIn,
9
- sourcesUnder,
10
- withChecker,
11
- withDependencies,
12
- } from './compileApp.ts'
2
+ import { type Placement, placement } from '#server/placement.ts'
3
+ import { type AppChecker, compiled } from './compileApp.ts'
13
4
  import { abideImportsOf } from './componentGraph.ts'
14
- import { projectShadow } from './projectShadow.ts'
15
- import {
16
- parseSources,
17
- type ReadSource,
18
- readSources,
19
- refusal,
20
- } from './readSources.ts'
5
+ import { hooksIn } from './hookCalls.ts'
6
+ import { parseAbide, scriptBodies } from './parseAbide.ts'
7
+ import { Refusals } from './Refusals.ts'
8
+ import { parseSources, readSources } from './readSources.ts'
21
9
  import type { Diagnosis } from './TypeSession.ts'
22
10
 
23
11
  /**
@@ -31,81 +19,21 @@ import type { Diagnosis } from './TypeSession.ts'
31
19
  * which is right for a build and wrong for a check: a file the parser or the read classifier
32
20
  * refuses is reported at its first line, and the rest of the app is checked around it. A refusal
33
21
  * only the whole app can see — two handlers at one address — comes from emitting it over the reads
34
- * already taken, which is asked only once every file has got that far on its own.
22
+ * of every file that got that far (`compiled`).
35
23
  */
36
24
  export async function checkApp(
37
25
  appRoot: string,
38
26
  kept?: AppChecker,
39
27
  ): Promise<Diagnosis[]> {
40
- const { root, sources } = await sourcesUnder(appRoot)
41
- return withChecker(root, kept, async (session) => {
42
- const refused: Diagnosis[] = []
43
- const own = parseSources(sources, refused)
44
- // 23.16-23.18 and 37.13 — what the build's walk would refuse of the tree, at each file it names.
45
- // 38.33 — each at its file, and every one rather than the first.
46
- const atFile = (relative: string, thrown: unknown): void => {
47
- refused.push(refusal(`${root}/${relative}`, thrown))
48
- }
49
- const { handlers, modules } = appFiles(root, atFile)
50
- refuseModuleHooks(root, modules, atFile)
51
- const resolve = resolverIn()
52
- const { sources: everyParsed, handlers: handlerFiles } =
53
- await withDependencies(root, own, handlers, resolve, refused)
54
- // 38.33 — A FILE WHOSE PROJECTION IS REFUSED (34.3, 34.17) is a finding at its file, found
55
- // here because every later step projects it and would throw the refusal for the whole app.
56
- // AFTER THE IMPORTS ARE FOLLOWED, so a refused file another one imports is found too.
57
- const parsed: typeof everyParsed = []
58
- for (const source of everyParsed)
59
- try {
60
- projectShadow(source.file)
61
- parsed.push(source)
62
- } catch (thrown) {
63
- refused.push(refusal(source.path, thrown))
64
- }
65
- const { read, warnings } = await readSources(session, parsed, refused)
66
- // THE APP'S OWN FILES ARE REPORTED ON: a dependency's is compiled with them (34.23), and
67
- // what is wrong inside a package is the package's to report.
68
- const reported: ReadSource[] = []
69
- for (const source of read)
70
- if (placeOf(relative(root, source.path)) !== 'dependency')
71
- reported.push(source)
72
- const { diagnoses: found, refused: loweringRefused } =
73
- await session.diagnose(reported, root)
74
- const { ambient, unkeyed } = await session.lints(root, parsed)
75
- // A FILE WHOSE LOWERING REFUSED is `found` already, at its line — emitted too, it throws the
76
- // same refusal at the root and takes the rest of the app's findings with it.
77
- const emittable: ReadSource[] = []
78
- for (const source of read)
79
- if (!loweringRefused.has(source.path)) emittable.push(source)
80
- if (refused.length === 0 && ambient.length === 0)
81
- try {
82
- warnings.push(
83
- ...(
84
- await emitApp(
85
- root,
86
- session,
87
- emittable,
88
- resolve,
89
- handlerFiles,
90
- atFile,
91
- )
92
- ).warnings,
93
- )
94
- } catch (thrown) {
95
- refused.push(refusal(root, thrown))
96
- }
97
- return [...refused, ...ambient, ...unkeyed, ...warnings, ...found]
98
- })
28
+ return (await compiled(appRoot, kept, true)).findings
99
29
  }
100
30
 
101
- const FILES_CONFIG = /^\s*onConfig\s*\(/m
102
-
103
31
  /**
104
32
  * The findings for one file as an editor holds it, saved or not.
105
33
  *
106
34
  * 38.11 — ONE FILE AS AN EDITOR HOLDS IT, saved or not: the same findings `checkApp` has for it, for
107
35
  * `abide lsp` to publish. The rest of the app is read from disk, as it stands — save a file `held`
108
- * has, which is one the editor holds too.
36
+ * has, which is one the editor holds too — and so is what the tree refuses of this one (38.34).
109
37
  *
110
38
  * 33.4 — THE COMPONENTS IT IMPORTS ARE LOWERED BESIDE IT, quietly, so a tag is held to the props its
111
39
  * component declares rather than to a stand-in that takes anything. One level: what a component
@@ -118,14 +46,24 @@ export async function checkSource(
118
46
  held: (path: string) => string | undefined = () => undefined,
119
47
  ): Promise<Diagnosis[]> {
120
48
  const { session, root } = checker
121
- const refused: Diagnosis[] = []
122
- const [page] = parseSources([{ path, text }], refused)
123
- if (page === undefined) return refused
49
+ const layout = await checker.layout()
50
+ // 38.33 — WHAT THE TREE REFUSES OF IT, off the layout the checker keeps until a move is heard.
51
+ const tree: Diagnosis[] = []
52
+ for (const refused of layout.refused)
53
+ if (refused.path === path) tree.push(refused)
54
+ const refusals = new Refusals(root, true)
55
+ const [page] = parseSources([{ path, text }], refusals)
56
+ if (page === undefined) return [...tree, ...refusals.found]
57
+ // 23.19 — WHERE THE APP PLACES IT, so a mounted page's `route.params` is typed by the route its
58
+ // mount gives it, as `checkApp` types it; a file the layout has not seen is placed at its path.
59
+ const placed: Pick<Placement, 'kind' | 'route'> =
60
+ layout.sources.find((source) => source.path === path) ??
61
+ placement(relative(root, path))
124
62
  const imports: { path: string; text: string; quiet: boolean }[] = []
125
63
  for (const imported of abideImportsOf(
126
64
  path,
127
65
  page.file,
128
- resolverIn(),
66
+ layout.resolve,
129
67
  ).values())
130
68
  try {
131
69
  imports.push({
@@ -137,26 +75,52 @@ export async function checkSource(
137
75
  // A component that cannot be read takes anything here.
138
76
  }
139
77
  // 27.13 — AND EVERY FILE FILING `onConfig`, quietly, so a page that only reads `config()` is
140
- // typed by defaults it never imported — as `checkApp`'s whole-app program types it. The scan
141
- // only names candidates (a statement opening on the call); the checker decides which file.
78
+ // typed by defaults it never imported — as `checkApp`'s whole-app program types it. The calls
79
+ // only name candidates; the checker decides which file.
142
80
  const lowered = new Set(imports.map((one) => one.path))
143
- const { sources } = await checker.sources()
144
- for (const other of sources) {
81
+ for (const other of layout.sources) {
145
82
  if (other.path === path || lowered.has(other.path)) continue
146
- const text = held(other.path) ?? other.text
147
- if (text.includes('onConfig') && FILES_CONFIG.test(text))
148
- imports.push({ path: other.path, text, quiet: true })
83
+ const editing = held(other.path)
84
+ if (
85
+ editing === undefined || editing === other.text
86
+ ? other.hooks.includes('onConfig')
87
+ : filesConfig(editing)
88
+ )
89
+ imports.push({
90
+ path: other.path,
91
+ text: editing ?? other.text,
92
+ quiet: true,
93
+ })
149
94
  }
150
95
  // A component that does not parse, or that the checker refuses, is its own file's finding and
151
96
  // takes anything here: a quiet source's refusal is reported by nobody.
152
97
  const { read, warnings } = await readSources(
153
98
  session,
154
- [page, ...parseSources(imports, refused)],
155
- refused,
99
+ [page, ...parseSources(imports, refusals)],
100
+ refusals,
156
101
  )
157
- if (refused.length > 0) return refused
102
+ if (refusals.found.length > 0) return [...tree, ...refusals.found]
158
103
  const own: Diagnosis[] = []
159
104
  for (const warning of warnings) if (warning.path === path) own.push(warning)
160
- const { diagnoses } = await session.diagnose(read, root, false)
161
- return [...own, ...diagnoses]
105
+ const [checked, ...beside] = read
106
+ const { diagnoses } = await session.diagnose(
107
+ checked === undefined
108
+ ? beside
109
+ : [
110
+ { ...checked, kind: placed.kind, route: placed.route },
111
+ ...beside,
112
+ ],
113
+ layout,
114
+ false,
115
+ )
116
+ return [...tree, ...own, ...diagnoses]
117
+ }
118
+
119
+ /** Whether a `.abide` an editor holds calls `onConfig`, where its text parses. */
120
+ function filesConfig(text: string): boolean {
121
+ try {
122
+ return hooksIn(scriptBodies(parseAbide(text))).includes('onConfig')
123
+ } catch {
124
+ return false
125
+ }
162
126
  }
@@ -1,12 +1,14 @@
1
1
  import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
2
2
  import type { CheckerAnswer } from './CheckerAnswer.ts'
3
3
  import type { Finding } from './Finding.ts'
4
+ import { hookCalls } from './hookCalls.ts'
4
5
  import {
5
6
  CHECKED_CONFIG,
6
7
  CHECKED_CONFIG_OF,
7
8
  CHECKED_VALUES,
8
9
  CHECKED_WRITE,
9
10
  Lowering,
11
+ type Routed,
10
12
  } from './Lowering.ts'
11
13
  import { holdsReactive } from './PositionTable.ts'
12
14
  import type { AbideFile } from './parseAbide.ts'
@@ -64,8 +66,10 @@ export const COMPONENT_MODULE = componentModule('any')
64
66
  export function checkedShadow(
65
67
  file: AbideFile,
66
68
  answer: CheckerAnswer,
69
+ /** 23.9 — a page's or a layout's route, which its `route.params` is typed by. */
70
+ routed?: Routed,
67
71
  ): CheckedShadow {
68
- const lowering = new Lowering(file, answer, true)
72
+ const lowering = new Lowering(file, answer, true, undefined, routed)
69
73
  const projection = lowering.projection
70
74
  // Every span the emitters lower, with whether it holds the `Reactive` rather than reading it.
71
75
  const units: { span: Span; holds: boolean }[] = [
@@ -165,13 +169,10 @@ ${aliases}declare module '${SERVER_SPECIFIER}' {
165
169
  `
166
170
  }
167
171
 
168
- // A call standing as a statement at a module's top level, as a formatted module writes one.
169
- const TOP_LEVEL_CONFIG = /^onConfig\s*\(/gm
170
-
171
172
  /**
172
173
  * 27.13 FOR A HANDLER FILE, the one module that is no `.abide` and may call a hook (37.13): its
173
- * text with each `onConfig(…)` at its top level bound as the lowering binds a server block's, and
174
- * `Config` extended by each, or `undefined` where it files none.
174
+ * text with each `onConfig(…)` standing as a statement at its top level bound as the lowering binds
175
+ * a server block's, and `Config` extended by each, or `undefined` where it files none.
175
176
  *
176
177
  * A MODULE OF ITS OWN BESIDE THE FILE, which the checker reads and nothing reports on: the file's own
177
178
  * text is the one its diagnostics are placed in, and binding a call shifts every column after it.
@@ -180,11 +181,12 @@ export function moduleConfigShadow(text: string): string | undefined {
180
181
  const bound: string[] = []
181
182
  let out = ''
182
183
  let from = 0
183
- for (const found of text.matchAll(TOP_LEVEL_CONFIG)) {
184
+ for (const call of hookCalls(text)) {
185
+ if (call.hook !== 'onConfig' || !call.topLevel) continue
184
186
  const name = `${CHECKED_CONFIG}${bound.length}`
185
187
  bound.push(name)
186
- out += `${text.slice(from, found.index)}const ${name} = ${CHECKED_CONFIG_OF}(onConfig)(`
187
- from = found.index + found[0].length
188
+ out += `${text.slice(from, call.at)}const ${name} = ${CHECKED_CONFIG_OF}(${text.slice(call.at, call.end)})`
189
+ from = call.end
188
190
  }
189
191
  if (bound.length === 0) return undefined
190
192
  return `${out}${text.slice(from)}\n${configInterface(bound)}`
@@ -101,11 +101,8 @@ export type Read = {
101
101
  local: boolean
102
102
  /** 33.20 — on the `props` call, the keys its pattern defaults; on a default's opening, its key. */
103
103
  keys: string[]
104
- /**
105
- * 23.9 — on `route.params` in a page or a layout, the route literal and which of the two the
106
- * file is.
107
- */
108
- routeParams: [pattern: string, kind: string] | undefined
104
+ /** 23.9 — the read is abide's `route.params`, which a page or a layout types by its route. */
105
+ routeParams: boolean
109
106
  }
110
107
 
111
108
  /**
@@ -136,7 +133,7 @@ export function newRead(
136
133
  byValue: false,
137
134
  local: false,
138
135
  keys: [],
139
- routeParams: undefined,
136
+ routeParams: false,
140
137
  }
141
138
  }
142
139