@brust/runtime-dom 2.0.0-alpha.4 → 2.0.0-alpha.6

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/README.md CHANGED
@@ -95,3 +95,13 @@ tracking state lives on `globalThis[Symbol.for('brust.reactive.ctx')]`).
95
95
  React-free.
96
96
  - `@brust/runtime-dom/store/internal`: the server-resolver hook and serialization helpers the Bun worker uses.
97
97
  Not part of the public surface.
98
+
99
+ ## Feature modules (L33, F139)
100
+
101
+ The runtime is a core plus feature modules, so a page loads only what its components use and the client JS does not grow as the compiler learns more of React.
102
+
103
+ - `@brust/runtime-dom/core` (`src/core.ts`): signals, `defineBehavior`/`setChunkLoader`, `mount`/`unmount`, the binder registry (`BINDERS`) and the internals features need (`src/core-api.ts`). It knows no directive by name.
104
+ - `@brust/runtime-dom/features/<name>` (`src/features/*.ts`): `basics` (`x-text`, `x-show`, `x-bind-*`, `x-on-*`, `x-ref`), `if`, `for`, `model`, `html`, `island` (React island hydration glue). A feature registers itself with the core when imported and imports the core only through `core-api`; in the browser build `core-api` is the shared core file, so there is one signal graph and one binder registry per page.
105
+ - `@brust/runtime-dom` (`src/index.ts`) is core + every feature, for tests and non-split consumers. The browser build never uses it.
106
+
107
+ **The rule for a new feature.** The micro-directives nearly every interactive page uses ship together in `basics`. Anything else gets its OWN module when it is at least about 1 KB gzip OR used by a minority of pages: `store`, `island`, `if`/`for`/`model`/`html` as measured, and every future feature (context, portal, focus trap, ...). Do not add a feature to `basics` or the core to save a file; add it as a module the compiler requests only for routes that use it. The CI byte budget (core + `basics`) holds the line.
package/package.json CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "@brust/runtime-dom",
3
- "version": "2.0.0-alpha.4",
3
+ "version": "2.0.0-alpha.6",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./src/index.ts",
8
8
  "default": "./src/index.ts"
9
9
  },
10
+ "./core": {
11
+ "types": "./src/core.ts",
12
+ "default": "./src/core.ts"
13
+ },
14
+ "./features/*": {
15
+ "types": "./src/features/*.ts",
16
+ "default": "./src/features/*.ts"
17
+ },
10
18
  "./store": {
11
19
  "types": "./src/store/index.ts",
12
20
  "default": "./src/store/index.ts"
@@ -49,5 +57,14 @@
49
57
  ],
50
58
  "publishConfig": {
51
59
  "access": "public"
60
+ },
61
+ "homepage": "https://brust.dev",
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "git+https://github.com/AssetsArt/brust.git",
65
+ "directory": "packages/runtime-dom"
66
+ },
67
+ "bugs": {
68
+ "url": "https://github.com/AssetsArt/brust/issues"
52
69
  }
53
70
  }
@@ -0,0 +1,10 @@
1
+ // What a feature module may import from the core, and nothing else. In the browser build the core is one shared
2
+ // ESM file and every feature is a separate file that imports these names from it (bundle.ts maps this module to the
3
+ // core URL), so a feature registers on the page's single binder registry and signal graph.
4
+ export { BINDERS, bindHost, bindElement, walkChildren } from './directives/index'
5
+ export type { Binder } from './directives/index'
6
+ export { parsed, read, member, mismatch, insideRaw, RAW_SEL } from './directives/common'
7
+ export { hooks, islandHooks } from './instance'
8
+ export { warnOnce } from './warn'
9
+ export { signal, computed, effect, batch, untracked, isSignal, isComputed } from './signal'
10
+ export type { Signal, Computed, Dispose } from './signal'
package/src/core.ts ADDED
@@ -0,0 +1,8 @@
1
+ // The always-loaded core: signals, the behavior registry, mount/unmount and the binder registry. Features
2
+ // (./features/*) register their binders on import; nothing here knows a directive by name.
3
+ export { signal, computed, effect, batch, untracked, isSignal, isComputed } from './signal'
4
+ export type { Signal, Computed, Dispose } from './signal'
5
+ export { defineBehavior, setChunkLoader } from './registry'
6
+ export type { BehaviorCtx, BehaviorFactory, BehaviorInstance } from './registry'
7
+ export { mount, unmount } from './mount'
8
+ export * from './core-api'
@@ -1,6 +1,4 @@
1
- import { BINDERS } from './index'
2
- import { parsed, read } from './common'
3
- import { warnOnce } from '../warn'
1
+ import { BINDERS, parsed, read, warnOnce } from '../core-api'
4
2
 
5
3
  // Attributes that execute or load code from a string; a bound value must never reach them as-is.
6
4
  const URL_ATTRS = new Set(['href', 'src', 'action', 'formaction', 'poster', 'data', 'xlink:href', 'ping'])
@@ -1,8 +1,4 @@
1
- import { BINDERS, bindElement, walkChildren } from './index'
2
- import { parsed, read, member } from './common'
3
- import { warnOnce } from '../warn'
4
- import { computed, signal, type Signal } from '../signal'
5
- import { hooks } from '../instance'
1
+ import { BINDERS, bindElement, walkChildren, parsed, read, member, warnOnce, computed, signal, hooks, type Signal } from '../core-api'
6
2
 
7
3
  type RowScope = Record<string, unknown> & { __own: Signal<number> }
8
4
 
@@ -1,5 +1,4 @@
1
- import { BINDERS } from './index'
2
- import { parsed, read, mismatch } from './common'
1
+ import { BINDERS, parsed, read, mismatch } from '../core-api'
3
2
  /** Both sides through the HTML parser (inert `<template>`), so `<br/>` vs `<br>` or entity spelling is not a mismatch. */
4
3
  function sameMarkup(el: Element, next: string): boolean {
5
4
  const t = document.createElement('template'); t.innerHTML = next
@@ -1,6 +1,4 @@
1
- import { BINDERS, bindElement, walkChildren } from './index'
2
- import { parsed, read } from './common'
3
- import { hooks } from '../instance'
1
+ import { BINDERS, bindElement, walkChildren, parsed, read, hooks } from '../core-api'
4
2
  BINDERS.push({
5
3
  match: (a) => a === 'x-if',
6
4
  bind: ({ inst, el, attr, raw, scope }) => {
@@ -1,7 +1,4 @@
1
- import { BINDERS } from './index'
2
- import { parsed, member } from './common'
3
- import { isSignal, type Signal } from '../signal'
4
- import { warnOnce } from '../warn'
1
+ import { BINDERS, parsed, member, isSignal, warnOnce, type Signal } from '../core-api'
5
2
  BINDERS.push({
6
3
  match: (a) => a === 'x-model',
7
4
  bind: ({ inst, el, attr, raw }) => {
@@ -1,5 +1,4 @@
1
- import { BINDERS } from './index'
2
- import { parsed, member } from './common'
1
+ import { BINDERS, parsed, member } from '../core-api'
3
2
  BINDERS.push({
4
3
  match: (a) => a.startsWith('x-on-'),
5
4
  bind: ({ inst, el, attr, raw, scope }) => {
@@ -1,4 +1,4 @@
1
- import { BINDERS } from './index'
1
+ import { BINDERS } from '../core-api'
2
2
  BINDERS.push({
3
3
  match: (a) => a === 'x-ref',
4
4
  bind: ({ inst, el, raw }) => {
@@ -1,5 +1,4 @@
1
- import { BINDERS } from './index'
2
- import { parsed, read } from './common'
1
+ import { BINDERS, parsed, read } from '../core-api'
3
2
  BINDERS.push({
4
3
  match: (a) => a === 'x-show',
5
4
  bind: ({ inst, el, attr, raw, scope }) => {
@@ -1,5 +1,4 @@
1
- import { BINDERS } from './index'
2
- import { parsed, read, mismatch } from './common'
1
+ import { BINDERS, parsed, read, mismatch } from '../core-api'
3
2
  BINDERS.push({
4
3
  match: (a) => a === 'x-text',
5
4
  bind: ({ inst, el, attr, raw, scope }) => {
@@ -0,0 +1,6 @@
1
+ // The directives nearly every interactive page uses, shipped as ONE module (see README "Feature modules").
2
+ import '../directives/text'
3
+ import '../directives/show'
4
+ import '../directives/bind'
5
+ import '../directives/on'
6
+ import '../directives/ref'
@@ -0,0 +1 @@
1
+ import '../directives/for'
@@ -0,0 +1 @@
1
+ import '../directives/html'
@@ -0,0 +1 @@
1
+ import '../directives/if'
@@ -0,0 +1,8 @@
1
+ // Every feature at once: what `@brust/runtime-dom` (the full entry) loads. The browser build never uses this file;
2
+ // it imports exactly the features a route needs.
3
+ import './basics'
4
+ import './if'
5
+ import './for'
6
+ import './model'
7
+ import './html'
8
+ import './island'
@@ -0,0 +1,9 @@
1
+ // React island hydration glue. Registers on the core's island hooks; hosts already in the document when this module
2
+ // arrives (the core mounts first) are scheduled right away.
3
+ import { islandHooks } from '../core-api'
4
+ import { scheduleIslands, cancelIslands } from '../island'
5
+ islandHooks.schedule = scheduleIslands
6
+ islandHooks.cancel = cancelIslands
7
+ if (typeof document !== 'undefined') scheduleIslands(document.documentElement)
8
+ export { defineIsland } from '../island'
9
+ export type { IslandHydrate } from '../island'
@@ -0,0 +1 @@
1
+ import '../directives/model'
@@ -0,0 +1,6 @@
1
+ // The store module: `defineStore` plus the reactive primitives a store's code imports (`@brust/core/store` aliases here),
2
+ // re-exported from the core so there is one signal graph per page.
3
+ export { signal, computed, effect, batch, untracked, isSignal, isComputed } from '../core-api'
4
+ export type { Signal, Computed, Dispose } from '../core-api'
5
+ export { defineStore } from '../store/define-store'
6
+ export type { StoreHandle, Snapshot } from '../store/define-store'
package/src/index.ts CHANGED
@@ -1,9 +1,8 @@
1
- export { signal, computed, effect, batch, untracked, isSignal, isComputed } from './signal'
2
- export type { Signal, Computed, Dispose } from './signal'
1
+ // The full runtime: the core plus every feature. Tests and non-split consumers import this; the browser build
2
+ // loads `./core` and only the `./features/*` a route needs.
3
+ export * from './core'
3
4
  export { defineStore } from './store/define-store'
4
5
  export type { StoreHandle, Snapshot } from './store/define-store'
5
- export { defineBehavior, setChunkLoader } from './registry'
6
- export type { BehaviorCtx, BehaviorFactory, BehaviorInstance } from './registry'
7
- export { mount, unmount } from './mount'
8
6
  export { defineIsland } from './island'
9
7
  export type { IslandHydrate } from './island'
8
+ import './features/index'
package/src/instance.ts CHANGED
@@ -93,3 +93,6 @@ export function nearestInstance(el: Element): Instance | null {
93
93
 
94
94
  /** Late-bound by mount.ts so binders (x-if, x-for) can mount nested hosts synchronously without an import cycle. */
95
95
  export const hooks: { mountTree: (root: ParentNode) => void } = { mountTree: () => {} }
96
+
97
+ /** Late-bound by the `island` feature (features/island.ts) so a page without React islands never loads the hydration glue. */
98
+ export const islandHooks: { schedule: (root: ParentNode) => void; cancel: (root: Node) => void } = { schedule: () => {}, cancel: () => {} }
package/src/island.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  // React islands: the server paints <brust-island data-id x-props>HTML</brust-island>; the island's
2
2
  // chunk registers a hydrate function through a global queue (no import of the runtime); the runtime
3
3
  // hydrates each host once, when idle (spec S12). Load order between runtime and chunks is free.
4
- import { warnOnce } from './warn'
5
- import { insideRaw } from './directives/common'
4
+ import { warnOnce, insideRaw } from './core-api'
6
5
 
7
6
  export type IslandHydrate = (host: HTMLElement, props: Record<string, unknown>) => void
8
7
  type QueueEntry = [string, IslandHydrate]
package/src/mount.ts CHANGED
@@ -1,12 +1,10 @@
1
- import { Instance, hooks, instances, nearestInstance } from './instance'
1
+ import { Instance, hooks, instances, islandHooks, nearestInstance } from './instance'
2
2
  import { untracked } from './signal'
3
3
  import { whenBehavior } from './registry'
4
4
  import { bindHost } from './directives/index'
5
5
  import { insideRaw } from './directives/common'
6
6
  import { warnOnce } from './warn'
7
7
  import { bindPropsFromParent } from './props-bind'
8
- import { scheduleIslands, cancelIslands } from './island'
9
- import './directives/register' // Tasks 3–6 add binders here (file created in Task 3; create it empty now)
10
8
 
11
9
  let observer: MutationObserver | null = null
12
10
 
@@ -44,13 +42,13 @@ function mountTree(root: ParentNode): void {
44
42
  root.querySelectorAll<HTMLElement>('[x-data]').forEach((h) => hosts.push(h))
45
43
  // document order = parents before children, so nearestInstance finds a mounted parent
46
44
  for (const h of hosts) if (!insideRaw(h)) mountHost(h)
47
- scheduleIslands(root)
45
+ islandHooks.schedule(root)
48
46
  }
49
47
  hooks.mountTree = (root) => untracked(() => mountTree(root))
50
48
 
51
49
  function disposeTree(root: Node): void {
52
50
  if (!(root instanceof Element)) return
53
- cancelIslands(root)
51
+ islandHooks.cancel(root)
54
52
  const hosts: Element[] = root.hasAttribute('x-data') ? [root] : []
55
53
  root.querySelectorAll('[x-data]').forEach((h) => hosts.push(h))
56
54
  for (const h of hosts) { const i = instances.get(h); if (i) { i.dispose(); instances.delete(h) } }
@@ -1,6 +1,6 @@
1
1
  // defineStore (M3-F D1): the 0.1.x `brustjs/store` handle on the shared reactive core and the
2
2
  // globalThis registry. Resolution (browser registry / server resolver) lives in ./registry.
3
- import { type Computed, type Signal, isSignal } from '../signal'
3
+ import { type Computed, type Signal, isSignal } from '../core-api'
4
4
  import { type StoreInstanceRecord, computeSnapshot, resolveRecord } from './registry'
5
5
 
6
6
  // Unwrap a Signal/Computed to its value type; `never` if it is neither. Uses `infer` (not
@@ -1,6 +1,6 @@
1
1
  // Browser store registry + the server-resolver hook (M3-F D3/D6/C7).
2
2
  // The registry lives on globalThis (C7) so two copies of this module share one instance per name.
3
- import { isComputed, isSignal, untracked } from '../signal'
3
+ import { isComputed, isSignal, untracked } from '../core-api'
4
4
  import { parseStoreScript } from './serialize'
5
5
 
6
6
  // The resolved per-scope store record, shared by the browser registry and the server per-request
@@ -1,9 +0,0 @@
1
- import './text'
2
- import './html'
3
- import './show'
4
- import './bind'
5
- import './on'
6
- import './if'
7
- import './model'
8
- import './for'
9
- import './ref'