@async/framework 0.11.14 → 0.11.16

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 CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.16 - 2026-06-19
4
+
5
+ - Added runtime slice entrypoints for `@async/framework/runtime`,
6
+ `@async/framework/runtime/signals`, and `@async/framework/runtime/events`.
7
+ - Added generated package artifacts and installed-package coverage for runtime
8
+ slice subpaths.
9
+ - Added deterministic scenario-size fixtures and checks for runtime, router,
10
+ server-call, and boundary receiver examples.
11
+ - Added package-owned release evidence checks before release ensure so generated
12
+ release workflows verify bundle and scenario evidence before publishing.
13
+ - Bundle size from bundled TypeScript source: `browser.ts` raw 197,173 B (197.2 KB / 0.197 MB), gzip 37,198 B (37.2 KB / 0.037 MB), br 30,914 B (30.9 KB / 0.031 MB) -> `browser.min.js` raw 84,013 B (84.0 KB / 0.084 MB), gzip 24,894 B (24.9 KB / 0.025 MB), br 22,079 B (22.1 KB / 0.022 MB); delta raw -113,160 B (-113.2 KB / -0.113 MB), gzip -12,304 B (-12.3 KB / -0.012 MB), br -8,835 B (-8.8 KB / -0.009 MB).
14
+
15
+ ## 0.11.15 - 2026-06-19
16
+
17
+ - Made the source package private and kept its public surface to the minimal
18
+ export spec for root, `/browser`, `/server`, and `/package.json` only.
19
+ - Moved publish staging to generated `dist/package.json` so npm and release
20
+ automation publish from `dist/` while package consumers still receive
21
+ root-level artifacts without `dist/` paths.
22
+ - Removed legacy direct artifact subpath exports plus top-level
23
+ `main`/`module`/`browser`/`types` and generated file lists from the source
24
+ manifest.
25
+ - Updated pack, size, pipeline, and installed-package coverage to verify the
26
+ browser and server entrypoints remain split after packing.
27
+ - Bundle size from bundled TypeScript source: `browser.ts` 197,173 B raw /
28
+ 37,198 B gzip -> `browser.min.js` 84,013 B raw / 24,894 B gzip
29
+ (-113,160 B raw, -12,304 B gzip).
30
+
3
31
  ## 0.11.14 - 2026-06-18
4
32
 
5
33
  - Added condition-specific root declaration targets so browser-conditioned root
@@ -10,9 +38,20 @@
10
38
  - Added packed-artifact export-map, declaration/runtime parity, and static
11
39
  import checks for root browser, root Node, explicit `/browser`, and explicit
12
40
  `/server` entrypoints.
13
- - Bundle size from bundled TypeScript source: `browser.ts` 187,564 B raw /
14
- 35,332 B gzip -> `browser.min.js` 80,009 B raw / 23,677 B gzip
15
- (-107,555 B raw, -11,655 B gzip).
41
+ - Added component-scoped continuous intersection helpers with
42
+ `this.intersect(...)` and `this.on("intersect", options?, fn)`, preserving
43
+ `on:visible` as a one-shot visibility lifecycle hook.
44
+ - Added declarative `on:intersect` with `intersect:threshold`,
45
+ `intersect:root-margin`, and `intersect:once` pseudo-event options, including
46
+ custom `intersect` attribute prefix support.
47
+ - Added observer cleanup coverage for component teardown, boundary swaps,
48
+ fallback scheduling, repeated entries, and existing visible compatibility.
49
+ - Moved root release artifacts to an ignored generated-output workflow:
50
+ tests, bundle checks, pack checks, and generated CI tasks materialize the
51
+ current package surface before verification or publish.
52
+ - Bundle size from bundled TypeScript source: `browser.ts` 197,173 B raw /
53
+ 37,198 B gzip -> `browser.min.js` 84,013 B raw / 24,894 B gzip
54
+ (-113,160 B raw, -12,304 B gzip).
16
55
 
17
56
  ## 0.11.13 - 2026-06-18
18
57
 
package/README.md CHANGED
@@ -43,10 +43,10 @@ Async.start({ root: document });
43
43
 
44
44
  ## What It Is
45
45
 
46
- `@async/framework` is the Layer 1 runtime plus the first Layer 2 app/server
47
- primitives. It keeps the runtime small and explicit:
46
+ `@async/framework` is the L1 runtime plus the first L1.5 app/server and
47
+ streaming primitives. It keeps the runtime small and explicit:
48
48
 
49
- - No build step for Layer 1 consumers.
49
+ - No build step for L1 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.
@@ -68,18 +68,14 @@ to the same runtime registries and HTML protocol.
68
68
  Async is designed as layers, so each level can stay useful without forcing the
69
69
  next level on every app.
70
70
 
71
- | Layer | Name | Requirement | Purpose |
71
+ | Shorthand | Name | Requirement | Purpose |
72
72
  | --- | --- | --- | --- |
73
- | 1 | Runtime bootloader | No build. CDN or direct ESM import. | Signals, async signals, scheduler, handlers, command events, lifecycle pseudo-events, scoped fragments, and boundary swaps. |
74
- | 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. |
75
- | 3 | Authoring build | Build step required. | JSX, ESM, and TypeScript authoring that lowers into Layer 1 HTML attributes and Layer 2 registries. |
76
- | 4 | Chunk and resumability metadata | Build metadata required. | Lazy module manifests, visibility/prefetch hints, resource graphs, and resumability records that the bootloader can consume. |
77
- | 5 | Framework compiler | Compiler required. | Server/client partitioning, code motion, optimized registry generation, serialized closures, and deeper resumability transforms. |
78
- | 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. |
73
+ | L1 | Runtime bootloader | No build. CDN or direct ESM import. | Signals, async signals, scheduler, handlers, command events, lifecycle pseudo-events, scoped fragments, and boundary swaps. |
74
+ | L1.5 | App/server and streaming bridge | Light server integration. No app compiler required. | `Async.use(...)`, router modes, server function proxy, partial registry, SSR output, browser activation, split browser/server cache, and streamed boundary patches. |
75
+ | L2 | Build-required authoring and compiler profile | Build step required. | JSX, ESM, and TypeScript authoring, optimizer reports, generated plans, generated registries, chunks, manifests, and future resumability records that lower onto L1 and L1.5 protocols. |
79
76
 
80
- The package in this repository intentionally focuses on Layers 1 and 2. Layers
81
- 3 through 6 are higher authoring surfaces, not extra runtime requirements for
82
- plain HTML apps.
77
+ The package in this repository intentionally focuses on L1 and L1.5. L2 is a
78
+ higher authoring surface, not an extra runtime requirement for plain HTML apps.
83
79
 
84
80
  ## Install
85
81
 
@@ -610,6 +606,10 @@ Loader scans regular HTML attributes:
610
606
  | `on:click="server.cart.add(productId)"` | Server command with signal args |
611
607
  | `on:attach="setup"` | Component root attach lifecycle pseudo-event |
612
608
  | `on:visible="trackView"` | Component root visible lifecycle pseudo-event |
609
+ | `on:intersect="trackSection"` | Continuous intersection lifecycle pseudo-event |
610
+ | `intersect:threshold="0,0.5,1"` | Intersection threshold option for `on:intersect` |
611
+ | `intersect:root-margin="-20% 0px -55% 0px"` | Intersection root margin option for `on:intersect` |
612
+ | `intersect:once="true"` | Disconnect `on:intersect` after the first intersecting entry |
613
613
  | `signal:text="product.title"` | Text binding |
614
614
  | `signal:value="productId"` | Form value binding with writeback |
615
615
  | `signal:attr:disabled="product.$loading"` | Attribute binding |
@@ -644,6 +644,7 @@ Async.start({
644
644
  attributes: {
645
645
  async: "data-async-",
646
646
  class: "data-class-",
647
+ intersect: "data-intersect-",
647
648
  signal: "data-signal-",
648
649
  on: "data-on-"
649
650
  }
@@ -651,7 +652,8 @@ Async.start({
651
652
  ```
652
653
 
653
654
  That maps to `data-async-container`, `data-on-click="save"`,
654
- `data-signal-text="product.title"`, and `data-class-selected="selected"`.
655
+ `data-signal-text="product.title"`, `data-class-selected="selected"`, and
656
+ `data-intersect-threshold="0.5"`.
655
657
 
656
658
  Inside `html` templates, signal refs can be passed directly to binding
657
659
  attributes:
@@ -1102,6 +1104,8 @@ Component helpers:
1102
1104
  | `this.on(event, fn)` | Fragment lifecycle fallback for `attach`, `visible`, and `destroy` |
1103
1105
  | `this.onMount(fn)` | Compatibility alias for `this.on("attach", fn)` |
1104
1106
  | `this.onVisible(fn)` | Compatibility alias for `this.on("visible", fn)` |
1107
+ | `this.on("intersect", options?, fn)` | Continuous intersection lifecycle for the mounted component scope |
1108
+ | `this.intersect(element, options?, fn)` | Component-owned continuous intersection observer for a direct element |
1105
1109
 
1106
1110
  `this.suspense(...)` is sugar for Loader boundaries:
1107
1111
  `asyncSignal + async:boundary + async:* templates`. It emits only templates. The
@@ -1176,6 +1180,73 @@ this.on("destroy", () => {
1176
1180
  the component root first becomes visible. Lifecycle events do not drive
1177
1181
  component rerenders.
1178
1182
 
1183
+ Use `on:intersect` when markup should receive continuous intersection updates
1184
+ through a registered handler:
1185
+
1186
+ ```html
1187
+ <section
1188
+ on:intersect="trackSection"
1189
+ intersect:threshold="0,0.25,0.5,0.75,1"
1190
+ intersect:root-margin="-20% 0px -55% 0px"
1191
+ >
1192
+ ...
1193
+ </section>
1194
+ ```
1195
+
1196
+ The handler receives `element`, `entry`, `entries`, `observer`,
1197
+ `isIntersecting`, `intersectionRatio`, and `unsupported`. Custom roots are not
1198
+ selector-based; use `this.intersect(...)` with a direct root element when a
1199
+ custom observer root is needed.
1200
+
1201
+ Use `this.on("intersect", ...)` when a component needs continuous visibility
1202
+ state:
1203
+
1204
+ ```js
1205
+ const Card = defineComponent(function Card() {
1206
+ const visible = this.signal(false);
1207
+
1208
+ this.on("intersect", { threshold: 0.5 }, ({ isIntersecting }) => {
1209
+ visible.set(isIntersecting);
1210
+ });
1211
+
1212
+ return html`<article class:visible="${visible}">...</article>`;
1213
+ });
1214
+ ```
1215
+
1216
+ Use `this.intersect(...)` with a direct element when a parent owns scroll-spy or
1217
+ active-section state:
1218
+
1219
+ ```js
1220
+ const Section = defineComponent(function Section({ id, observeSection }) {
1221
+ const attach = this.handler("attach", function ({ element }) {
1222
+ return observeSection(id, element);
1223
+ });
1224
+
1225
+ return html`<section on:attach="${attach}"><h2>${id}</h2></section>`;
1226
+ });
1227
+
1228
+ const Page = defineComponent(function Page() {
1229
+ const active = this.signal("intro");
1230
+ const ratios = new Map();
1231
+ const options = {
1232
+ rootMargin: "-20% 0px -55% 0px",
1233
+ threshold: [0, 0.25, 0.5, 0.75, 1]
1234
+ };
1235
+
1236
+ const observeSection = (id, element) => this.intersect(element, options, ({ entry }) => {
1237
+ ratios.set(id, entry.isIntersecting ? entry.intersectionRatio : 0);
1238
+ const best = [...ratios.entries()].sort((a, b) => b[1] - a[1])[0];
1239
+ active.set(best?.[0] ?? id);
1240
+ });
1241
+
1242
+ return html`
1243
+ <nav signal:text="${active}"></nav>
1244
+ ${this.render(Section, { id: "intro", observeSection })}
1245
+ ${this.render(Section, { id: "runtime", observeSection })}
1246
+ `;
1247
+ });
1248
+ ```
1249
+
1179
1250
  ## Streaming
1180
1251
 
1181
1252
  Out-of-order HTML can target a boundary and keep delegated handlers working:
@@ -1264,6 +1335,8 @@ pnpm run pipeline:github:check
1264
1335
  Useful commands:
1265
1336
 
1266
1337
  ```bash
1338
+ pnpm run bundle
1339
+ pnpm run bundle:clean
1267
1340
  pnpm run pipeline:verify
1268
1341
  pnpm run pipeline:pages
1269
1342
  pnpm run registry:lint
@@ -1271,6 +1344,20 @@ pnpm run pipeline:release:doctor
1271
1344
  pnpm run release:check
1272
1345
  ```
1273
1346
 
1347
+ Release artifacts such as `browser.js`, `browser.min.js`,
1348
+ `browser.umd.min.js`, `browser.ts`, `browser.d.ts`, `framework.ts`,
1349
+ `framework.d.ts`, and `server.js` are generated into `dist/`. The generated
1350
+ `dist/` directory is the package root for `npm pack` and release publishing, so
1351
+ the published package and CDN surface still expose those files at package root
1352
+ rather than under `dist/`. The source `package.json` stays private and owns the
1353
+ minimal public export spec, while omitting legacy `main`/`module`/`browser`
1354
+ entry fields and generated package file lists. `scripts/build-framework-bundle.js`
1355
+ derives the generated `dist/package.json` and staged artifact names from that
1356
+ spec. Feature branches should edit source files and let `pnpm run bundle`,
1357
+ `pnpm test`, `pnpm run pack:check`, or the generated release workflow
1358
+ materialize the publish tree. Use `pnpm run bundle:clean` to remove local
1359
+ generated artifacts after inspection.
1360
+
1274
1361
  `registry:lint` scans package source and examples for declared registry ids
1275
1362
  such as signals, handlers, server functions, partials, routes, and components.
1276
1363
  It writes `.async/registry-manifest.json` plus a per-file cache at
package/browser.d.ts CHANGED
@@ -23,6 +23,7 @@ export interface AttributeConfig {
23
23
  async?: string | string[];
24
24
  class?: string | string[];
25
25
  signal?: string | string[];
26
+ intersect?: string | string[];
26
27
  on?: string | string[];
27
28
  }
28
29
 
@@ -30,6 +31,7 @@ export interface NormalizedAttributeConfig {
30
31
  async: string[];
31
32
  class: string[];
32
33
  signal: string[];
34
+ intersect: string[];
33
35
  on: string[];
34
36
  }
35
37
 
@@ -403,7 +405,41 @@ export interface Router {
403
405
  destroy(): void;
404
406
  }
405
407
 
406
- export type LifecycleEventName = "attach" | "mount" | "visible" | "destroy";
408
+ export type LifecycleEventName = "attach" | "mount" | "visible" | "intersect" | "destroy";
409
+
410
+ export interface IntersectionFallbackEntry {
411
+ target: Element;
412
+ isIntersecting: boolean;
413
+ intersectionRatio: number;
414
+ time: number;
415
+ rootBounds: DOMRectReadOnly | null;
416
+ boundingClientRect: DOMRect | DOMRectReadOnly | null;
417
+ intersectionRect: DOMRect | DOMRectReadOnly | null;
418
+ }
419
+
420
+ export interface IntersectionEvent {
421
+ target: Element;
422
+ element: Element;
423
+ el: Element;
424
+ root: Document | Element | DocumentFragment;
425
+ entry: IntersectionObserverEntry | IntersectionFallbackEntry;
426
+ entries: Array<IntersectionObserverEntry | IntersectionFallbackEntry>;
427
+ observer: IntersectionObserver | null;
428
+ isIntersecting: boolean;
429
+ intersectionRatio: number;
430
+ unsupported: boolean;
431
+ }
432
+
433
+ export interface IntersectionOptions {
434
+ root?: Element | Document | null;
435
+ rootMargin?: string;
436
+ threshold?: number | number[];
437
+ once?: boolean;
438
+ schedule?: "lifecycle" | "sync";
439
+ key?: string;
440
+ }
441
+
442
+ export type IntersectionCallback = (this: ComponentContext, event: IntersectionEvent) => unknown;
407
443
 
408
444
  export interface ComponentContext {
409
445
  scope: string;
@@ -423,9 +459,13 @@ export interface ComponentContext {
423
459
  handler(name: string, fn: HandlerFunction): string;
424
460
  render<TProps extends Record<string, unknown> = Record<string, unknown>>(Child: ComponentFunction<TProps>, props?: TProps): TemplateLike;
425
461
  suspense(signalRef: Pick<SignalRef, "id">, views: SuspenseViews | SuspenseReadyView): TemplateLike;
426
- on(eventName: LifecycleEventName, fn: (this: ComponentContext, target?: Element) => unknown): void;
462
+ on(eventName: "intersect", fn: IntersectionCallback): void;
463
+ on(eventName: "intersect", options: IntersectionOptions | undefined | null, fn: IntersectionCallback): void;
464
+ on(eventName: Exclude<LifecycleEventName, "intersect">, fn: (this: ComponentContext, target?: Element) => unknown): void;
427
465
  onMount(fn: (this: ComponentContext, target?: Element) => unknown): void;
428
466
  onVisible(fn: (this: ComponentContext, target?: Element) => unknown): void;
467
+ intersect(target: Element, fn: IntersectionCallback): Cleanup;
468
+ intersect(target: Element, options: IntersectionOptions | undefined | null, fn: IntersectionCallback): Cleanup;
429
469
  }
430
470
 
431
471
  export type ComponentFunction<TProps extends Record<string, unknown> = Record<string, unknown>> = (this: ComponentContext, props: TProps) => TemplateLike;