@abide/abide 0.31.0 → 0.32.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 (35) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/package.json +1 -2
  4. package/src/checkAbide.ts +11 -6
  5. package/src/lib/ui/compile/AbideCompileError.ts +16 -0
  6. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +0 -1
  7. package/src/lib/ui/compile/abideUiPlugin.ts +25 -2
  8. package/src/lib/ui/compile/bindListenEvent.ts +19 -0
  9. package/src/lib/ui/compile/compileShadow.ts +22 -5
  10. package/src/lib/ui/compile/generateBuild.ts +110 -213
  11. package/src/lib/ui/compile/generateSSR.ts +51 -86
  12. package/src/lib/ui/compile/lowerContext.ts +64 -0
  13. package/src/lib/ui/compile/lowerDocAccess.ts +6 -1
  14. package/src/lib/ui/compile/offsetToLineColumn.ts +16 -0
  15. package/src/lib/ui/compile/scopeAttr.ts +9 -0
  16. package/src/lib/ui/compile/staticAttr.ts +11 -0
  17. package/src/lib/ui/compile/staticTextPart.ts +12 -0
  18. package/src/lib/ui/compile/unwrapParens.ts +10 -0
  19. package/src/lib/ui/dom/awaitBlock.ts +27 -21
  20. package/src/lib/ui/dom/clearBetween.ts +16 -0
  21. package/src/lib/ui/dom/each.ts +64 -38
  22. package/src/lib/ui/dom/eachAsync.ts +39 -52
  23. package/src/lib/ui/dom/fillBefore.ts +16 -0
  24. package/src/lib/ui/dom/moveRange.ts +19 -0
  25. package/src/lib/ui/dom/openMarker.ts +22 -0
  26. package/src/lib/ui/dom/removeRange.ts +18 -0
  27. package/src/lib/ui/dom/switchBlock.ts +32 -40
  28. package/src/lib/ui/dom/tryBlock.ts +31 -35
  29. package/src/lib/ui/dom/types/EachRow.ts +10 -3
  30. package/src/lib/ui/dom/types/SwitchCase.ts +3 -2
  31. package/src/lib/ui/dom/when.ts +34 -43
  32. package/src/lib/ui/installHotBridge.ts +0 -2
  33. package/src/lib/ui/state.ts +14 -5
  34. package/src/lib/ui/compile/branchElements.ts +0 -50
  35. package/src/lib/ui/dom/openRoot.ts +0 -20
package/AGENTS.md CHANGED
@@ -174,7 +174,7 @@ Same selector grammar as `cache.invalidate`; also accept a `Subscribable`. See C
174
174
  - `abide/ui/router(...)`, `abide/ui/startClient(...)`, `abide/ui/renderToStream(render)` — bootstrap/render runtime (compiler/launcher uses these).
175
175
 
176
176
  ### DOM + render runtime — `@readme plumbing` (compiler-emitted; you don't hand-write these)
177
- `abide/ui/dom/{mount,mountChild,hydrate,text,openChild,openRoot,appendText,appendSnippet,appendStatic,cloneStatic,attr,on,attach,each,eachAsync,when,awaitBlock,tryBlock,switchBlock,applyResolved}` and `abide/ui/runtime/{nextBlockId,enterRenderPass,exitRenderPass}`. These are what `analyzeComponent → generateBuild/generateSSR` lower a `.abide` file into. Read them only to understand compiler output.
177
+ `abide/ui/dom/{mount,mountChild,hydrate,text,openChild,appendText,appendSnippet,appendStatic,cloneStatic,attr,on,attach,each,eachAsync,when,awaitBlock,tryBlock,switchBlock,applyResolved}` and `abide/ui/runtime/{nextBlockId,enterRenderPass,exitRenderPass}`. These are what `analyzeComponent → generateBuild/generateSSR` lower a `.abide` file into. Read them only to understand compiler output.
178
178
  - `abide/ui/remoteProxy`, `abide/ui/socketProxy` — the browser-side implementations the bundler swaps in for `GET(...)` / `socket(...)`.
179
179
 
180
180
  ### `.abide` component format (see `src/lib/ui/README.md`)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # abide
2
2
 
3
+ ## 0.32.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`786a496`](https://github.com/briancray/abide/commit/786a4965fa3c94954ea8d66aa41b0f94e0ec8320) - render control-flow branches into marker-bounded ranges ([`23efa06`](https://github.com/briancray/abide/commit/23efa0606b90b69ff262679673d660a71baf443c))
8
+
9
+ ### Patch Changes
10
+
11
+ - [`786a496`](https://github.com/briancray/abide/commit/786a4965fa3c94954ea8d66aa41b0f94e0ec8320) - report checked component count on success ([`9ea83bb`](https://github.com/briancray/abide/commit/9ea83bb3c072d72e67486906b191964505e872f2))
12
+
13
+ ## 0.31.1
14
+
15
+ ### Patch Changes
16
+
17
+ - [`a9f7b3b`](https://github.com/briancray/abide/commit/a9f7b3b09f1db15fa784844a2672b1058dde2b25) - stop the type-check shadow merging a semicolon-less call into the next component ([`7d863d7`](https://github.com/briancray/abide/commit/7d863d7b1f2d4d973a5eafd94cf5002fdeb519c4))
18
+
3
19
  ## 0.31.0
4
20
 
5
21
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Isomorphic multimodal HTTP framework built for humans and machines in a single Bun runtime",
@@ -82,7 +82,6 @@
82
82
  "./ui/dom/hydrate": "./src/lib/ui/dom/hydrate.ts",
83
83
  "./ui/dom/text": "./src/lib/ui/dom/text.ts",
84
84
  "./ui/dom/openChild": "./src/lib/ui/dom/openChild.ts",
85
- "./ui/dom/openRoot": "./src/lib/ui/dom/openRoot.ts",
86
85
  "./ui/dom/appendText": "./src/lib/ui/dom/appendText.ts",
87
86
  "./ui/dom/appendSnippet": "./src/lib/ui/dom/appendSnippet.ts",
88
87
  "./ui/dom/appendStatic": "./src/lib/ui/dom/appendStatic.ts",
package/src/checkAbide.ts CHANGED
@@ -14,7 +14,7 @@ the same as each package checked on its own — and the same as the LSP. Returns
14
14
  the error count so the CLI can set its exit code.
15
15
  */
16
16
  export async function checkAbide({ cwd }: { cwd: string }): Promise<number> {
17
- const diagnostics = collectByProject(cwd)
17
+ const { diagnostics, checked } = collectByProject(cwd)
18
18
  const byFile = new Map<string, AbideDiagnostic[]>()
19
19
  for (const diagnostic of diagnostics) {
20
20
  const bucket = byFile.get(diagnostic.file) ?? []
@@ -32,11 +32,14 @@ export async function checkAbide({ cwd }: { cwd: string }): Promise<number> {
32
32
  const errors = diagnostics.filter(
33
33
  (diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error,
34
34
  ).length
35
- const relative = byFile.size
35
+ /* Success reports components *checked* (the glob count); failure reports the
36
+ files *with* errors. Reporting `byFile.size` on success printed `0` — it only
37
+ holds files that had diagnostics. */
38
+ const fileCount = byFile.size
36
39
  console.log(
37
40
  errors === 0
38
- ? `\n[abide check] no type errors in ${relative} component${relative === 1 ? '' : 's'}`
39
- : `\n[abide check] ${errors} error${errors === 1 ? '' : 's'} in ${relative} file${relative === 1 ? '' : 's'}`,
41
+ ? `\n[abide check] no type errors in ${checked} component${checked === 1 ? '' : 's'}`
42
+ : `\n[abide check] ${errors} error${errors === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}`,
40
43
  )
41
44
  return errors
42
45
  }
@@ -45,7 +48,7 @@ export async function checkAbide({ cwd }: { cwd: string }): Promise<number> {
45
48
  project's components against that project's options, then concatenates the
46
49
  diagnostics. Imported components from another project resolve on demand through
47
50
  the host, so the per-project root set stays each project's own files. */
48
- function collectByProject(cwd: string): AbideDiagnostic[] {
51
+ function collectByProject(cwd: string): { diagnostics: AbideDiagnostic[]; checked: number } {
49
52
  const byProject = new Map<string, string[]>()
50
53
  for (const relative of new Bun.Glob('**/*.abide').scanSync({ cwd, onlyFiles: true })) {
51
54
  if (relative.includes('node_modules')) {
@@ -55,9 +58,11 @@ function collectByProject(cwd: string): AbideDiagnostic[] {
55
58
  const root = nearestProjectRoot(path, cwd)
56
59
  byProject.set(root, [...(byProject.get(root) ?? []), path])
57
60
  }
58
- return [...byProject].flatMap(([root, paths]) =>
61
+ const diagnostics = [...byProject].flatMap(([root, paths]) =>
59
62
  collectAbideDiagnostics(createShadowProgram(root, paths)),
60
63
  )
64
+ const checked = [...byProject.values()].reduce((total, paths) => total + paths.length, 0)
65
+ return { diagnostics, checked }
61
66
  }
62
67
 
63
68
  /* Renders one diagnostic as `path:line:col severity message` plus the offending
@@ -0,0 +1,16 @@
1
+ /*
2
+ A compile-time `.abide` error carrying the absolute source offset of the
3
+ offending node (when the parser tracked one). The loader catches it, resolves the
4
+ offset to `line:col` against the file, and re-throws with the component path and
5
+ position in the message — so a failed build names the exact file and line, not the
6
+ entry page at offset 0 (Bun frames plugin throws at `<file>:0` regardless).
7
+ */
8
+ export class AbideCompileError extends Error {
9
+ readonly offset: number | undefined
10
+
11
+ constructor(message: string, offset?: number) {
12
+ super(message)
13
+ this.name = 'AbideCompileError'
14
+ this.offset = offset
15
+ }
16
+ }
@@ -15,7 +15,6 @@ export const UI_RUNTIME_IMPORTS: { name: string; specifier: string }[] = [
15
15
  { name: 'effect', specifier: 'ui/effect' },
16
16
  { name: 'mount', specifier: 'ui/dom/mount' },
17
17
  { name: 'openChild', specifier: 'ui/dom/openChild' },
18
- { name: 'openRoot', specifier: 'ui/dom/openRoot' },
19
18
  { name: 'appendText', specifier: 'ui/dom/appendText' },
20
19
  { name: 'appendSnippet', specifier: 'ui/dom/appendSnippet' },
21
20
  { name: 'appendStatic', specifier: 'ui/dom/appendStatic' },
@@ -1,8 +1,10 @@
1
1
  import { relative } from 'node:path'
2
2
  import type { BunPlugin } from 'bun'
3
+ import { AbideCompileError } from './AbideCompileError.ts'
3
4
  import { analyzeComponent } from './analyzeComponent.ts'
4
5
  import { compileModule } from './compileModule.ts'
5
6
  import { nearestProjectRoot } from './nearestProjectRoot.ts'
7
+ import { offsetToLineColumn } from './offsetToLineColumn.ts'
6
8
 
7
9
  /*
8
10
  Bun plugin that loads `.abide` single-file components: compiles each to the ES
@@ -40,11 +42,32 @@ export const abideUiPlugin: BunPlugin = {
40
42
  const source = await Bun.file(args.path).text()
41
43
  const moduleId = relative(nearestProjectRoot(args.path, process.cwd()), args.path)
42
44
  const isLayout = (args.path.split('/').pop() ?? '') === 'layout.abide'
43
- const code = compileModule(source, { isLayout, moduleId })
45
+ /* Bun frames a plugin throw at `<file>:0` regardless of the real spot, so
46
+ carry the component path + resolved line:col in the message — otherwise a
47
+ control-flow / compile error reads as `:0` and (in deep imports) can look
48
+ like it came from the entry page rather than this component. */
49
+ const compileAbide = <T>(step: () => T): T => {
50
+ try {
51
+ return step()
52
+ } catch (error) {
53
+ const offset = error instanceof AbideCompileError ? error.offset : undefined
54
+ const at =
55
+ offset === undefined
56
+ ? moduleId
57
+ : (({ line, column }) => `${moduleId}:${line}:${column}`)(
58
+ offsetToLineColumn(source, offset),
59
+ )
60
+ const message = error instanceof Error ? error.message : String(error)
61
+ throw new Error(`${message.replace(/^\[abide\]\s*/, `[abide] ${at} — `)}`)
62
+ }
63
+ }
64
+ const code = compileAbide(() => compileModule(source, { isLayout, moduleId }))
44
65
  /* Browser build with `<style>`(s): concatenate every scoped block's CSS and
45
66
  pull it into the bundle via one virtual import, keyed by `moduleId` so the
46
67
  registry id and the CSS id agree. */
47
- const styles = toBrowser ? analyzeComponent(source, moduleId).styles : []
68
+ const styles = compileAbide(() =>
69
+ toBrowser ? analyzeComponent(source, moduleId).styles : [],
70
+ )
48
71
  if (styles.length === 0) {
49
72
  return { contents: code, loader: 'ts' }
50
73
  }
@@ -0,0 +1,19 @@
1
+ /*
2
+ The DOM event a generic two-way `bind:<property>` listens on to write the bound
3
+ path back. Most form fields report edits via `input`, but a few properties are
4
+ driven by their own event and never fire `input` — `<details open>` fires
5
+ `toggle`, a checkbox/radio `checked` fires `change`, and a `<select>` settles its
6
+ `value` on `change`. Picking the wrong event silently breaks the write-back.
7
+ */
8
+ export function bindListenEvent(property: string, tag: string): string {
9
+ if (property === 'open') {
10
+ return 'toggle'
11
+ }
12
+ if (property === 'checked') {
13
+ return 'change'
14
+ }
15
+ if (property === 'value' && tag === 'select') {
16
+ return 'change'
17
+ }
18
+ return 'input'
19
+ }
@@ -229,7 +229,14 @@ function scopeLineFor(
229
229
  const annotation = typeNode === undefined ? '' : `: ${verbatim(typeNode)}`
230
230
  const init = call.arguments[0]
231
231
  if (init === undefined) {
232
- return { text: `let ${name}${annotation};`, segments: [] }
232
+ /* No initial (`state<T>()`): the value is `T | undefined`. A definite-
233
+ assignment assertion (`!`) gives that union without a use-before-assign
234
+ false-positive AND without control-flow narrowing it to just `undefined`
235
+ (an `= undefined` initializer, never reassigned in the shadow, would make
236
+ a guard like `x !== undefined` collapse to `never`). Unguarded access is
237
+ then correctly flagged possibly-undefined; a guard narrows cleanly. */
238
+ const valueType = annotation === '' ? ': unknown' : `${annotation} | undefined`
239
+ return { text: `let ${name}!${valueType};`, segments: [] }
233
240
  }
234
241
  const prefix = `let ${name}${annotation} = (`
235
242
  return { text: `${prefix}${verbatim(init)});`, segments: [span(init, prefix.length)] }
@@ -332,8 +339,13 @@ function emitNode(node: TemplateNode, builder: Builder): void {
332
339
  offending expression (an annotated target reports the error on the RHS,
333
340
  unlike an object literal which reports it on the key). */
334
341
  for (const prop of node.props) {
342
+ /* Lead with a defensive `;`: this IIFE is the one shadow emission that
343
+ starts with `(`, so without it a preceding scope statement left
344
+ unterminated (a script ending in a call with no trailing semicolon,
345
+ e.g. `effect(() => …)`) merges across the newline into `effect(…)(…)`
346
+ — a spurious "not callable" on the author's last statement. */
335
347
  builder.raw(
336
- `((__prop: Parameters<typeof ${node.name}>[0][${JSON.stringify(prop.name)}]) => {})(`,
348
+ `;((__prop: Parameters<typeof ${node.name}>[0][${JSON.stringify(prop.name)}]) => {})(`,
337
349
  )
338
350
  builder.expr(prop.code, prop.loc)
339
351
  builder.raw(');\n')
@@ -452,9 +464,14 @@ function emitNode(node: TemplateNode, builder: Builder): void {
452
464
  builder.raw('};\n')
453
465
  return
454
466
  case 'script':
455
- /* A scoped reactive `<script>` block its body is author TS; emit it so
456
- its references check. Not yet position-mapped (rare). */
457
- builder.raw(`{\n${node.code}\n}\n`)
467
+ /* A scoped reactive `<script>`: emit its body INLINE in the current block,
468
+ not a nested `{…}` — so its bindings are visible to the branch's later
469
+ siblings (a nested if/each within the same branch), matching runtime
470
+ scope where a nested script's declarations deref through the rest of the
471
+ branch. A wrapping block trapped them, surfacing "Cannot find name" on a
472
+ sibling. Leading `;` guards a preceding semicolon-less call from merging
473
+ in (see the component case). Not yet position-mapped (rare). */
474
+ builder.raw(`;\n${node.code}\n`)
458
475
  return
459
476
  case 'style':
460
477
  /* CSS, not TypeScript — nothing for the shadow to type-check. */