@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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +16 -0
- package/package.json +1 -2
- package/src/checkAbide.ts +11 -6
- package/src/lib/ui/compile/AbideCompileError.ts +16 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +0 -1
- package/src/lib/ui/compile/abideUiPlugin.ts +25 -2
- package/src/lib/ui/compile/bindListenEvent.ts +19 -0
- package/src/lib/ui/compile/compileShadow.ts +22 -5
- package/src/lib/ui/compile/generateBuild.ts +110 -213
- package/src/lib/ui/compile/generateSSR.ts +51 -86
- package/src/lib/ui/compile/lowerContext.ts +64 -0
- package/src/lib/ui/compile/lowerDocAccess.ts +6 -1
- package/src/lib/ui/compile/offsetToLineColumn.ts +16 -0
- package/src/lib/ui/compile/scopeAttr.ts +9 -0
- package/src/lib/ui/compile/staticAttr.ts +11 -0
- package/src/lib/ui/compile/staticTextPart.ts +12 -0
- package/src/lib/ui/compile/unwrapParens.ts +10 -0
- package/src/lib/ui/dom/awaitBlock.ts +27 -21
- package/src/lib/ui/dom/clearBetween.ts +16 -0
- package/src/lib/ui/dom/each.ts +64 -38
- package/src/lib/ui/dom/eachAsync.ts +39 -52
- package/src/lib/ui/dom/fillBefore.ts +16 -0
- package/src/lib/ui/dom/moveRange.ts +19 -0
- package/src/lib/ui/dom/openMarker.ts +22 -0
- package/src/lib/ui/dom/removeRange.ts +18 -0
- package/src/lib/ui/dom/switchBlock.ts +32 -40
- package/src/lib/ui/dom/tryBlock.ts +31 -35
- package/src/lib/ui/dom/types/EachRow.ts +10 -3
- package/src/lib/ui/dom/types/SwitchCase.ts +3 -2
- package/src/lib/ui/dom/when.ts +34 -43
- package/src/lib/ui/installHotBridge.ts +0 -2
- package/src/lib/ui/state.ts +14 -5
- package/src/lib/ui/compile/branchElements.ts +0 -50
- 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,
|
|
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.
|
|
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
|
-
|
|
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 ${
|
|
39
|
-
: `\n[abide check] ${errors} error${errors === 1 ? '' : 's'} in ${
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
456
|
-
its
|
|
457
|
-
|
|
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. */
|