@async/framework 0.3.0 → 0.4.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/CHANGELOG.md +13 -0
- package/README.md +181 -34
- package/examples/components/main.js +10 -10
- package/examples/ssr/main.js +1 -1
- package/examples/streaming/main.js +1 -1
- package/framework.js +3912 -0
- package/package.json +9 -5
- package/src/attributes.js +2 -0
- package/src/component.js +83 -15
- package/src/html.js +99 -6
- package/src/loader.js +223 -12
- package/src/partials.js +15 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-06-17
|
|
4
|
+
|
|
5
|
+
- Added a generated root `framework.js` ESM bundle for UNPKG browser imports.
|
|
6
|
+
- Expanded the README with Async layer definitions and an htmx comparison.
|
|
7
|
+
- Added `on:attach` as the canonical component attach lifecycle pseudo-event
|
|
8
|
+
with `on:mount` kept as a compatibility alias.
|
|
9
|
+
- Added top-level `class:*` bindings, including aggregate `class:`
|
|
10
|
+
string/object/array class sets.
|
|
11
|
+
- Added inline `html` template bindings for signal refs, class arrays/objects,
|
|
12
|
+
`value="${signalRef}"`, and generated component handlers via
|
|
13
|
+
`this.handler(fn)`.
|
|
14
|
+
- Added generated component-local signals via `this.signal(initial)`.
|
|
15
|
+
|
|
3
16
|
## 0.3.0 - 2026-06-17
|
|
4
17
|
|
|
5
18
|
- Added a shared registry store behind `Async`, app runtimes, and concrete
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# @async/framework
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Async is a layered framework plan that starts as a no-build browser bootloader:
|
|
4
|
+
signals, async signals, delegated command events, scoped fragment components,
|
|
5
|
+
server calls, route partials, and out-of-order boundary swaps without a virtual
|
|
6
|
+
DOM.
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
9
|
pnpm add @async/framework
|
|
@@ -43,10 +43,10 @@ Async.start({ root: document });
|
|
|
43
43
|
|
|
44
44
|
## What It Is
|
|
45
45
|
|
|
46
|
-
`@async/framework` is the
|
|
47
|
-
runtime small and explicit:
|
|
46
|
+
`@async/framework` is the Layer 1 runtime plus the first Layer 2 app/server
|
|
47
|
+
primitives. It keeps the runtime small and explicit:
|
|
48
48
|
|
|
49
|
-
- No build step for consumers.
|
|
49
|
+
- No build step for Layer 1 consumers.
|
|
50
50
|
- No virtual DOM, diff path, hydration runtime, or component rerender loop.
|
|
51
51
|
- Signals are the state boundary.
|
|
52
52
|
- `Async.use(...)` registers app declarations before or after startup.
|
|
@@ -57,8 +57,27 @@ runtime small and explicit:
|
|
|
57
57
|
- Boundaries can be swapped out of order and rescanned, which keeps server
|
|
58
58
|
streaming and partial HTML replacement simple.
|
|
59
59
|
|
|
60
|
-
Higher layers can
|
|
61
|
-
|
|
60
|
+
Higher layers can add JSX lowering, TypeScript, chunk manifests, compiler-owned
|
|
61
|
+
server/client splits, and intent-first authoring later. They should compile down
|
|
62
|
+
to the same runtime registries and HTML protocol.
|
|
63
|
+
|
|
64
|
+
## Layers
|
|
65
|
+
|
|
66
|
+
Async is designed as layers, so each level can stay useful without forcing the
|
|
67
|
+
next level on every app.
|
|
68
|
+
|
|
69
|
+
| Layer | Name | Requirement | Purpose |
|
|
70
|
+
| --- | --- | --- | --- |
|
|
71
|
+
| 1 | Runtime bootloader | No build. CDN or direct ESM import. | Signals, async signals, handlers, command events, lifecycle pseudo-events, scoped fragments, and boundary swaps. |
|
|
72
|
+
| 2 | App/server layer | Light server integration. No app compiler required. | `Async.use(...)`, router modes, server function proxy, partial registry, SSR output, browser activation, and split browser/server cache. |
|
|
73
|
+
| 3 | Authoring build | Build step required. | JSX, ESM, and TypeScript authoring that lowers into Layer 1 HTML attributes and Layer 2 registries. |
|
|
74
|
+
| 4 | Chunk and resumability metadata | Build metadata required. | Lazy module manifests, visibility/prefetch hints, resource graphs, and resumability records that the bootloader can consume. |
|
|
75
|
+
| 5 | Framework compiler | Compiler required. | Server/client partitioning, code motion, optimized registry generation, serialized closures, and deeper resumability transforms. |
|
|
76
|
+
| 6 | TSRX and intent layer | Higher-level compiler required. | More declarative author intent, AI/compiler-friendly metadata, and source forms that generate lower-layer Async apps. |
|
|
77
|
+
|
|
78
|
+
The package in this repository intentionally focuses on Layers 1 and 2. Layers
|
|
79
|
+
3 through 6 are higher authoring surfaces, not extra runtime requirements for
|
|
80
|
+
plain HTML apps.
|
|
62
81
|
|
|
63
82
|
## Install
|
|
64
83
|
|
|
@@ -71,9 +90,9 @@ and package lifecycle tooling. Browser consumers import ESM directly.
|
|
|
71
90
|
|
|
72
91
|
## CDN
|
|
73
92
|
|
|
74
|
-
The package
|
|
75
|
-
step. Use `@latest` for quick prototypes, and pin an exact
|
|
76
|
-
production:
|
|
93
|
+
The package ships a root `framework.js` ESM bundle for UNPKG and can be loaded
|
|
94
|
+
without a build step. Use `@latest` for quick prototypes, and pin an exact
|
|
95
|
+
version in production:
|
|
77
96
|
|
|
78
97
|
```html
|
|
79
98
|
<main async:container>
|
|
@@ -85,7 +104,7 @@ production:
|
|
|
85
104
|
import {
|
|
86
105
|
Async,
|
|
87
106
|
createSignal
|
|
88
|
-
} from "https://unpkg.com/@async/framework@latest";
|
|
107
|
+
} from "https://unpkg.com/@async/framework@latest/framework.js";
|
|
89
108
|
|
|
90
109
|
Async.use({
|
|
91
110
|
signal: {
|
|
@@ -108,7 +127,7 @@ You can also use an import map so app code imports `@async/framework` by name:
|
|
|
108
127
|
<script type="importmap">
|
|
109
128
|
{
|
|
110
129
|
"imports": {
|
|
111
|
-
"@async/framework": "https://unpkg.com/@async/framework@latest"
|
|
130
|
+
"@async/framework": "https://unpkg.com/@async/framework@latest/framework.js"
|
|
112
131
|
}
|
|
113
132
|
}
|
|
114
133
|
</script>
|
|
@@ -365,10 +384,13 @@ AsyncLoader scans regular HTML attributes:
|
|
|
365
384
|
| `on:click="selectProduct"` | Delegated command event |
|
|
366
385
|
| `on:submit="preventDefault; save"` | Sequential command chain |
|
|
367
386
|
| `on:click="server.cart.add(productId)"` | Server command with signal args |
|
|
387
|
+
| `on:attach="setup"` | Component root attach lifecycle pseudo-event |
|
|
388
|
+
| `on:visible="trackView"` | Component root visible lifecycle pseudo-event |
|
|
368
389
|
| `signal:text="product.title"` | Text binding |
|
|
369
390
|
| `signal:value="productId"` | Form value binding with writeback |
|
|
370
391
|
| `signal:attr:disabled="product.$loading"` | Attribute binding |
|
|
371
|
-
| `
|
|
392
|
+
| `class:selected="selected"` | Class toggle from a signal path |
|
|
393
|
+
| `signal:class="buttonClasses"` | Class set from a signal value: string, object, or array |
|
|
372
394
|
| `async:boundary="product"` | Async or streamed replacement boundary |
|
|
373
395
|
| `async:loading="product"` | Boundary loading template |
|
|
374
396
|
| `async:ready="product"` | Boundary ready template |
|
|
@@ -396,19 +418,92 @@ Async.start({
|
|
|
396
418
|
root: document,
|
|
397
419
|
attributes: {
|
|
398
420
|
async: "data-async-",
|
|
421
|
+
class: "data-class-",
|
|
399
422
|
signal: "data-signal-",
|
|
400
423
|
on: "data-on-"
|
|
401
424
|
}
|
|
402
425
|
});
|
|
403
426
|
```
|
|
404
427
|
|
|
405
|
-
That maps to `data-async-container`, `data-on-click="save"`,
|
|
406
|
-
`data-signal-text="product.title"`.
|
|
428
|
+
That maps to `data-async-container`, `data-on-click="save"`,
|
|
429
|
+
`data-signal-text="product.title"`, and `data-class-selected="selected"`.
|
|
430
|
+
|
|
431
|
+
Named class toggles use their own top-level namespace:
|
|
432
|
+
|
|
433
|
+
```html
|
|
434
|
+
<button
|
|
435
|
+
class="button"
|
|
436
|
+
class:selected="selected"
|
|
437
|
+
>
|
|
438
|
+
Add
|
|
439
|
+
</button>
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Aggregate class binding uses `signal:class`. It reads the current signal value
|
|
443
|
+
and accepts strings, objects, and arrays:
|
|
444
|
+
|
|
445
|
+
```js
|
|
446
|
+
Async.use({
|
|
447
|
+
signal: {
|
|
448
|
+
buttonClasses: createSignal([
|
|
449
|
+
"button-primary",
|
|
450
|
+
{ selected: true, disabled: false },
|
|
451
|
+
["compact"]
|
|
452
|
+
])
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
```html
|
|
458
|
+
<button signal:class="buttonClasses">Add</button>
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Inside `html` templates, `signal:class` can also receive objects or arrays
|
|
462
|
+
directly. Signal refs inside the object or array are tracked:
|
|
463
|
+
|
|
464
|
+
```js
|
|
465
|
+
const selected = this.signal("selected", false);
|
|
466
|
+
const tone = this.signal("tone", "primary");
|
|
467
|
+
|
|
468
|
+
return html`
|
|
469
|
+
<article signal:class="${["card", tone, { selected }]}"}>
|
|
470
|
+
...
|
|
471
|
+
</article>
|
|
472
|
+
`;
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
For component-local state that does not need a stable public id, omit the name.
|
|
476
|
+
The signal is still registered under the component scope:
|
|
477
|
+
|
|
478
|
+
```js
|
|
479
|
+
const selected = this.signal(false);
|
|
480
|
+
const tone = this.signal("primary");
|
|
481
|
+
|
|
482
|
+
return html`
|
|
483
|
+
<article signal:class="${["card", selected, tone]}">
|
|
484
|
+
...
|
|
485
|
+
</article>
|
|
486
|
+
`;
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`value="${signalRef}"` in an `html` template is equivalent to adding
|
|
490
|
+
`signal:value` for that signal. It writes back on input/change:
|
|
491
|
+
|
|
492
|
+
```js
|
|
493
|
+
const productId = this.signal("productId", "sku-1");
|
|
494
|
+
|
|
495
|
+
return html`<input value="${productId}">`;
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
`signal:class:selected="selected"` remains supported as a compatibility alias,
|
|
499
|
+
but new examples should use `class:selected`. The parser-safe top-level
|
|
500
|
+
aggregate form `class:="buttonClasses"` also remains supported.
|
|
407
501
|
|
|
408
502
|
### Command Events
|
|
409
503
|
|
|
410
|
-
`on:*` works with any native DOM event name. `on:
|
|
411
|
-
reserved pseudo-events with cleanup support.
|
|
504
|
+
`on:*` works with any native DOM event name. `on:attach` and `on:visible` are
|
|
505
|
+
reserved component lifecycle pseudo-events with cleanup support. `on:mount`
|
|
506
|
+
remains as a compatibility alias for `on:attach`.
|
|
412
507
|
|
|
413
508
|
Command chains use semicolons and are awaited sequentially:
|
|
414
509
|
|
|
@@ -674,21 +769,25 @@ type and no rerender loop.
|
|
|
674
769
|
|
|
675
770
|
```js
|
|
676
771
|
const Toggle = defineComponent(function Toggle() {
|
|
677
|
-
const selected = this.signal(
|
|
678
|
-
const
|
|
679
|
-
|
|
772
|
+
const selected = this.signal(false);
|
|
773
|
+
const attach = this.handler("attach", function ({ element }) {
|
|
774
|
+
element.dataset.attached = "true";
|
|
680
775
|
});
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
target.dataset.mounted = "true";
|
|
776
|
+
const visible = this.handler("visible", function ({ element }) {
|
|
777
|
+
element.dataset.visible = "true";
|
|
684
778
|
});
|
|
685
779
|
|
|
686
780
|
return html`
|
|
687
781
|
<button
|
|
688
782
|
type="button"
|
|
689
|
-
on:
|
|
690
|
-
|
|
691
|
-
|
|
783
|
+
on:attach="${attach}"
|
|
784
|
+
on:visible="${visible}"
|
|
785
|
+
on:click="${this.handler(function () {
|
|
786
|
+
selected.update((value) => !value);
|
|
787
|
+
})}"
|
|
788
|
+
class:selected="${selected}"
|
|
789
|
+
signal:class="${["toggle", { active: selected }]}"
|
|
790
|
+
signal:attr:aria-pressed="${selected}"
|
|
692
791
|
>
|
|
693
792
|
Toggle
|
|
694
793
|
</button>
|
|
@@ -705,17 +804,46 @@ Component helpers:
|
|
|
705
804
|
|
|
706
805
|
| Helper | Behavior |
|
|
707
806
|
| --- | --- |
|
|
708
|
-
| `this.signal(name, initial)` | Scoped get-or-create signal |
|
|
807
|
+
| `this.signal(name, initial)` | Scoped named get-or-create signal |
|
|
808
|
+
| `this.signal(initial)` | Generated scoped local signal |
|
|
709
809
|
| `this.computed(name, fn)` | Scoped computed signal |
|
|
710
810
|
| `this.asyncSignal(name, fn)` | Scoped async signal |
|
|
711
811
|
| `this.effect(fn)` | Scoped effect with cleanup |
|
|
712
|
-
| `this.handler(name, fn)` | Scoped handler registry entry |
|
|
812
|
+
| `this.handler(name, fn)` | Scoped named handler registry entry |
|
|
813
|
+
| `this.handler(fn)` | Generated scoped handler registry entry |
|
|
713
814
|
| `this.render(Component, props)` | Child fragment rendering |
|
|
714
|
-
| `this.
|
|
715
|
-
| `this.
|
|
815
|
+
| `this.on(event, fn)` | Fragment lifecycle fallback for `attach`, `visible`, and `destroy` |
|
|
816
|
+
| `this.onMount(fn)` | Compatibility alias for `this.on("attach", fn)` |
|
|
817
|
+
| `this.onVisible(fn)` | Compatibility alias for `this.on("visible", fn)` |
|
|
716
818
|
|
|
717
|
-
|
|
718
|
-
|
|
819
|
+
Put component lifecycle on the component root element when there is one:
|
|
820
|
+
|
|
821
|
+
```js
|
|
822
|
+
const attach = this.handler("attach", function ({ element }) {
|
|
823
|
+
element.dataset.attached = "true";
|
|
824
|
+
});
|
|
825
|
+
const visible = this.handler("visible", function ({ element }) {
|
|
826
|
+
element.dataset.visible = "true";
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
return html`<article on:attach="${attach}" on:visible="${visible}">...</article>`;
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
If a component returns text or multiple root nodes, use the scoped fallback:
|
|
833
|
+
|
|
834
|
+
```js
|
|
835
|
+
this.on("attach", (target) => {
|
|
836
|
+
target.dataset.attached = "true";
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
this.on("destroy", () => {
|
|
840
|
+
// Clean up fragment-scoped resources.
|
|
841
|
+
});
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
`on:visible` is defined as a component lifecycle pseudo-event. It runs once when
|
|
845
|
+
the component root first becomes visible. Lifecycle events do not drive
|
|
846
|
+
component rerenders.
|
|
719
847
|
|
|
720
848
|
## Streaming
|
|
721
849
|
|
|
@@ -782,3 +910,22 @@ then runs release doctor.
|
|
|
782
910
|
The core runtime is intentionally small. Bundling, lazy chunk manifests, JSX
|
|
783
911
|
lowering, TSRX lowering, server resource compilation, and higher-level
|
|
784
912
|
resumability metadata are deferred to later layers.
|
|
913
|
+
|
|
914
|
+
## Async And htmx
|
|
915
|
+
|
|
916
|
+
Async and htmx are both HTML-first and avoid a virtual DOM, but they optimize
|
|
917
|
+
for different boundaries.
|
|
918
|
+
|
|
919
|
+
| Area | htmx | Async |
|
|
920
|
+
| --- | --- | --- |
|
|
921
|
+
| Primary model | HTML attributes issue HTTP requests and swap server responses. | HTML attributes bind signals, command events, server calls, and route boundaries. |
|
|
922
|
+
| State | Server-owned hypermedia state; browser state is intentionally minimal. | Browser signal registry plus server signal patches and cache snapshots. |
|
|
923
|
+
| Server interaction | DOM attributes describe HTTP verbs, targets, and swaps. | `server.*(...)` commands call registered server functions and apply returned effects. |
|
|
924
|
+
| Routing | Usually server navigation or htmx-boosted navigation. | CSR, SPA, SSR, SSR-SPA, and MPA router modes built around partial boundaries. |
|
|
925
|
+
| Components | Server-rendered HTML fragments. | Scoped fragment functions today; higher layers can compile JSX/TSRX later. |
|
|
926
|
+
| Build story | No build by default. | Layer 1 is no-build/CDN; higher layers can add build or compiler steps. |
|
|
927
|
+
|
|
928
|
+
Use htmx when the server should own most interaction through hypermedia and
|
|
929
|
+
HTTP swaps. Use Async when you want an HTML-first runtime that also has local
|
|
930
|
+
signals, async resources, registered browser/server handlers, route partials,
|
|
931
|
+
and a path to higher compiler layers without changing the Layer 1 protocol.
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { AsyncLoader, defineComponent, html } from "../../src/index.js";
|
|
2
2
|
|
|
3
3
|
const Toggle = defineComponent(function Toggle() {
|
|
4
|
-
const selected = this.signal(
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
this.onMount(() => {
|
|
10
|
-
document.body.dataset.toggleMounted = "true";
|
|
4
|
+
const selected = this.signal(false);
|
|
5
|
+
const attach = this.handler("attach", function ({ element }) {
|
|
6
|
+
element.dataset.attached = "true";
|
|
11
7
|
});
|
|
12
8
|
|
|
13
9
|
return html`
|
|
14
10
|
<button
|
|
15
11
|
type="button"
|
|
16
|
-
on:
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
on:attach="${attach}"
|
|
13
|
+
on:click="${this.handler(function () {
|
|
14
|
+
selected.update((value) => !value);
|
|
15
|
+
})}"
|
|
16
|
+
class:selected="${selected}"
|
|
17
|
+
signal:class="${["toggle", { active: selected }]}"
|
|
18
|
+
signal:attr:aria-pressed="${selected}"
|
|
19
19
|
>
|
|
20
20
|
Toggle
|
|
21
21
|
</button>
|
package/examples/ssr/main.js
CHANGED
|
@@ -56,7 +56,7 @@ serverApp.use({
|
|
|
56
56
|
<article>
|
|
57
57
|
<h1>${product.title}</h1>
|
|
58
58
|
<p>${product.id}</p>
|
|
59
|
-
<button type="button" on:click="ssrDemo.selectProduct"
|
|
59
|
+
<button type="button" on:click="ssrDemo.selectProduct" class:selected="ssrDemo.selected">
|
|
60
60
|
Select
|
|
61
61
|
</button>
|
|
62
62
|
</article>
|
|
@@ -14,7 +14,7 @@ Async.use({
|
|
|
14
14
|
`
|
|
15
15
|
<article>
|
|
16
16
|
<h1 signal:text="streamingDemo.title"></h1>
|
|
17
|
-
<button type="button" on:click="streamingDemo.select"
|
|
17
|
+
<button type="button" on:click="streamingDemo.select" class:selected="streamingDemo.selected">
|
|
18
18
|
Select
|
|
19
19
|
</button>
|
|
20
20
|
</article>
|