brustjs 0.1.65-alpha → 0.1.67-alpha

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brustjs",
3
- "version": "0.1.65-alpha",
3
+ "version": "0.1.67-alpha",
4
4
  "description": "Bun + Rust SSR framework — React on the server, Rust everywhere else (napi cdylib + per-worker SharedArrayBuffer).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,12 +41,12 @@
41
41
  "typescript": "^6.0.3"
42
42
  },
43
43
  "optionalDependencies": {
44
- "brustjs-darwin-x64": "0.1.65-alpha",
45
- "brustjs-darwin-arm64": "0.1.65-alpha",
46
- "brustjs-linux-x64-gnu": "0.1.65-alpha",
47
- "brustjs-linux-arm64-gnu": "0.1.65-alpha",
48
- "brustjs-linux-x64-musl": "0.1.65-alpha",
49
- "brustjs-linux-arm64-musl": "0.1.65-alpha"
44
+ "brustjs-darwin-x64": "0.1.67-alpha",
45
+ "brustjs-darwin-arm64": "0.1.67-alpha",
46
+ "brustjs-linux-x64-gnu": "0.1.67-alpha",
47
+ "brustjs-linux-arm64-gnu": "0.1.67-alpha",
48
+ "brustjs-linux-x64-musl": "0.1.67-alpha",
49
+ "brustjs-linux-arm64-musl": "0.1.67-alpha"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": "^19.2.6",
@@ -0,0 +1,85 @@
1
+ import ts from 'typescript'
2
+
3
+ /**
4
+ * Return true only when the entry contains a statically provable
5
+ * `brust.run({ ..., ai: true })` call through a named import from `brustjs`.
6
+ */
7
+ export function entryHasLiteralAiOptIn(entry: string): boolean {
8
+ const program = ts.createProgram({
9
+ rootNames: [entry],
10
+ options: {
11
+ allowJs: true,
12
+ jsx: ts.JsxEmit.Preserve,
13
+ noLib: true,
14
+ noResolve: true,
15
+ target: ts.ScriptTarget.Latest,
16
+ },
17
+ })
18
+ const source = program.getSourceFile(entry)
19
+ if (!source) return false
20
+ const checker = program.getTypeChecker()
21
+ const brustBindings = new Set<ts.Symbol>()
22
+
23
+ for (const statement of source.statements) {
24
+ if (
25
+ !ts.isImportDeclaration(statement) ||
26
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
27
+ statement.moduleSpecifier.text !== 'brustjs'
28
+ ) {
29
+ continue
30
+ }
31
+ const importClause = statement.importClause
32
+ if (!importClause || importClause.isTypeOnly) continue
33
+ const bindings = importClause.namedBindings
34
+ if (!bindings || !ts.isNamedImports(bindings)) continue
35
+ for (const element of bindings.elements) {
36
+ if (element.isTypeOnly) continue
37
+ if ((element.propertyName ?? element.name).text === 'brust') {
38
+ const symbol = checker.getSymbolAtLocation(element.name)
39
+ if (symbol) brustBindings.add(symbol)
40
+ }
41
+ }
42
+ }
43
+
44
+ let enabled = false
45
+ const visit = (node: ts.Node): void => {
46
+ if (enabled) return
47
+ const receiver =
48
+ ts.isCallExpression(node) &&
49
+ ts.isPropertyAccessExpression(node.expression) &&
50
+ ts.isIdentifier(node.expression.expression)
51
+ ? node.expression.expression
52
+ : undefined
53
+ const receiverSymbol = receiver ? checker.getSymbolAtLocation(receiver) : undefined
54
+ if (
55
+ ts.isCallExpression(node) &&
56
+ ts.isPropertyAccessExpression(node.expression) &&
57
+ node.expression.name.text === 'run' &&
58
+ receiverSymbol &&
59
+ brustBindings.has(receiverSymbol)
60
+ ) {
61
+ const options = node.arguments[0]
62
+ if (options && ts.isObjectLiteralExpression(options) && hasLiteralAiTrue(options)) {
63
+ enabled = true
64
+ return
65
+ }
66
+ }
67
+ ts.forEachChild(node, visit)
68
+ }
69
+ visit(source)
70
+ return enabled
71
+ }
72
+
73
+ function hasLiteralAiTrue(options: ts.ObjectLiteralExpression): boolean {
74
+ let initializer: ts.Expression | undefined
75
+ for (const property of options.properties) {
76
+ if (ts.isSpreadAssignment(property)) return false
77
+ const name = property.name
78
+ const propertyName =
79
+ name && (ts.isIdentifier(name) || ts.isStringLiteral(name)) ? name.text : undefined
80
+ if (propertyName !== 'ai') continue
81
+ if (!ts.isPropertyAssignment(property)) return false
82
+ initializer = property.initializer
83
+ }
84
+ return initializer?.kind === ts.SyntaxKind.TrueKeyword
85
+ }
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'
5
5
  import path, { isAbsolute, resolve } from 'node:path'
6
6
  import type { BunPlugin } from 'bun'
7
7
  import { extractAiManifest, writeManifest as writeAiManifest } from '../ai/manifest.ts'
8
+ import { entryHasLiteralAiOptIn } from './ai-opt-in.ts'
8
9
  import { emitNativeTemplates } from './native-routes-emit.ts'
9
10
  import { nativeShimPlugin } from './native-shim-plugin.ts'
10
11
 
@@ -251,14 +252,14 @@ export async function runBuild(args: string[]): Promise<void> {
251
252
  process.exit(1)
252
253
  }
253
254
  const { entry, outDir, target } = parsed
254
- const aiEnabled = parsed.ai || process.env.BRUST_AI === '1'
255
- if (aiEnabled) process.env.BRUST_AI = '1'
256
255
 
257
256
  // Entry existence is a runBuild concern (parseArgs stays fs-free/pure).
258
257
  if (!existsSync(entry)) {
259
258
  console.error(`brust build: no entry file at ${entry}; pass a path or create ./index.ts`)
260
259
  process.exit(1)
261
260
  }
261
+ const aiEnabled = parsed.ai || process.env.BRUST_AI === '1' || entryHasLiteralAiOptIn(entry)
262
+ if (aiEnabled) process.env.BRUST_AI = '1'
262
263
  const entryDir = path.dirname(entry)
263
264
 
264
265
  console.log(`[brust build] entry: ${entry}`)
@@ -4,7 +4,7 @@ import { createRequire } from 'node:module'
4
4
  import { dirname, relative, resolve } from 'node:path'
5
5
  import { buildDevClientTag } from '../dev/client.ts'
6
6
  import {
7
- aiScriptTag,
7
+ injectAiScriptIntoTemplate,
8
8
  insertGeneratorMeta,
9
9
  insertShellMeta,
10
10
  resolveGenerator,
@@ -259,11 +259,8 @@ export function countMainTags(template: string): number {
259
259
  *
260
260
  * Exported for the md emit step (runtime/md/emit.ts), which bakes the same tag
261
261
  * under its `withDevClient` option — md pages render Rust-side too, so without
262
- * it they never auto-reload in dev.
263
- *
264
- * The AI runtime script is injected here as well when BRUST_AI=1. The tag is
265
- * document-only: the compiler emits a head anchor for full documents, while
266
- * fragment templates (no head) are left unchanged. */
262
+ * it they never auto-reload in dev. AI injection is deliberately separate —
263
+ * production AI must never pull in this reconnect/overlay client. */
267
264
  export function injectDevClientIntoTemplate(template: string): string {
268
265
  const devTag = buildDevClientTag()
269
266
  let out = template
@@ -275,15 +272,6 @@ export function injectDevClientIntoTemplate(template: string): string {
275
272
  out += devTag
276
273
  }
277
274
  }
278
- if (process.env.BRUST_AI === '1') {
279
- const tag = aiScriptTag()
280
- if (!out.includes(tag)) {
281
- const headClose = out.indexOf('</head>')
282
- if (headClose !== -1) {
283
- out = out.slice(0, headClose) + tag + out.slice(headClose)
284
- }
285
- }
286
- }
287
275
  return out
288
276
  }
289
277
 
@@ -1002,10 +990,10 @@ export async function emitNativeTemplates(opts: NativeRouteEmitOpts): Promise<Na
1002
990
  // routes.ts makeFlat (never recomputed here), so native + React documents
1003
991
  // carry the identical signature. Empty/missing → no-op.
1004
992
  const withShell = insertShellMeta(withGenerator, r.shellId ?? '')
993
+ const withDevClient =
994
+ process.env.BRUST_DEV === '1' ? injectDevClientIntoTemplate(withShell) : withShell
1005
995
  const template =
1006
- process.env.BRUST_DEV === '1' || process.env.BRUST_AI === '1'
1007
- ? injectDevClientIntoTemplate(withShell)
1008
- : withShell
996
+ process.env.BRUST_AI === '1' ? injectAiScriptIntoTemplate(withDevClient) : withDevClient
1009
997
  writeFileSync(outPath, template)
1010
998
  built.push(name)
1011
999
  stats.compiled++
@@ -8,6 +8,8 @@ export interface CoordinatorDeps {
8
8
  }
9
9
  buildCss: () => Promise<void>
10
10
  buildIslands: () => Promise<void>
11
+ /** Parse changed JS/TS modules before mutating the live generation. */
12
+ validateChanges: (paths: string[]) => Promise<void>
11
13
  /** Recompile native-route `.jinja` templates from source and reload them into
12
14
  * the minijinja env, so `native: true` routes pick up .tsx edits on reload. */
13
15
  reEmitJinja: () => Promise<void>
@@ -22,16 +24,72 @@ export interface CoordinatorDeps {
22
24
  tui: { appendEvent(line: string): void }
23
25
  }
24
26
 
25
- type State = 'idle' | 'building'
27
+ type BuildDomain = 'full' | 'app-css' | 'component-css'
28
+
29
+ interface PendingBatch {
30
+ kind: ChangeKind
31
+ paths: Set<string>
32
+ }
33
+
34
+ const DOMAIN_PRIORITY: BuildDomain[] = ['full', 'app-css', 'component-css']
26
35
 
27
36
  export class Coordinator {
28
- private state: State = 'idle'
37
+ private readonly pending = new Map<BuildDomain, PendingBatch>()
38
+ private drainPromise: Promise<void> | null = null
29
39
 
30
40
  constructor(private deps: CoordinatorDeps) {}
31
41
 
32
- async handleChange(ev: { paths: string[]; kind: ChangeKind }): Promise<void> {
33
- if (this.state === 'building') return
34
- this.state = 'building'
42
+ handleChange(ev: { paths: string[]; kind: ChangeKind }): Promise<void> {
43
+ const domain = domainFor(ev.kind)
44
+ const existing = this.pending.get(domain)
45
+ if (existing) {
46
+ for (const path of ev.paths) existing.paths.add(path)
47
+ } else {
48
+ this.pending.set(domain, { kind: ev.kind, paths: new Set(ev.paths) })
49
+ }
50
+
51
+ if (!this.drainPromise) {
52
+ this.startDrain()
53
+ }
54
+ return this.drainPromise!
55
+ }
56
+
57
+ private startDrain(): Promise<void> {
58
+ // Defer ownership by one microtask so the watcher's ordered callbacks for
59
+ // a mixed debounce window can coalesce into the three bounded domains.
60
+ const drain = Promise.resolve().then(() => this.drainPending())
61
+ let tracked!: Promise<void>
62
+ tracked = drain.finally(() => {
63
+ if (this.drainPromise !== tracked) return
64
+ this.drainPromise = null
65
+ // A callback can enqueue after drainPending observes an empty queue but
66
+ // before this finalizer runs. Chain its replacement drain so callers of
67
+ // the finishing drain still wait for all accepted work.
68
+ if (this.pending.size > 0) return this.startDrain()
69
+ })
70
+ this.drainPromise = tracked
71
+ return tracked
72
+ }
73
+
74
+ private async drainPending(): Promise<void> {
75
+ while (true) {
76
+ const event = this.takeNext()
77
+ if (!event) return
78
+ await this.runBatch(event)
79
+ }
80
+ }
81
+
82
+ private takeNext(): { paths: string[]; kind: ChangeKind } | null {
83
+ for (const domain of DOMAIN_PRIORITY) {
84
+ const batch = this.pending.get(domain)
85
+ if (!batch) continue
86
+ this.pending.delete(domain)
87
+ return { kind: batch.kind, paths: Array.from(batch.paths) }
88
+ }
89
+ return null
90
+ }
91
+
92
+ private async runBatch(ev: { paths: string[]; kind: ChangeKind }): Promise<void> {
35
93
  const started = performance.now()
36
94
  try {
37
95
  await this.deps.broadcast({ type: 'building' })
@@ -46,6 +104,7 @@ export class Coordinator {
46
104
  // per-isolate (islands/native-render.ts), so a re-emitted
47
105
  // .islands.json sidecar is never re-read by a live worker.
48
106
  case 'md':
107
+ await this.deps.validateChanges(ev.paths)
49
108
  // Stale frozen island renders must not survive a source edit.
50
109
  this.deps.clearIslandCache?.()
51
110
  // Rebuild island CLIENT chunks. The watcher classifies every `.tsx`
@@ -103,12 +162,16 @@ export class Coordinator {
103
162
  message: e.message ?? String(e),
104
163
  stack: e.stack,
105
164
  })
106
- } finally {
107
- this.state = 'idle'
108
165
  }
109
166
  }
110
167
  }
111
168
 
169
+ function domainFor(kind: ChangeKind): BuildDomain {
170
+ if (kind === 'css') return 'app-css'
171
+ if (kind === 'component-css') return 'component-css'
172
+ return 'full'
173
+ }
174
+
112
175
  function formatStart(ev: { paths: string[]; kind: ChangeKind }): string {
113
176
  const icon = ev.kind === 'css' ? '⎈' : '⏵'
114
177
  const label =
@@ -0,0 +1,33 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ const LOADERS = new Map<string, 'ts' | 'tsx' | 'js' | 'jsx'>([
5
+ ['.ts', 'ts'],
6
+ ['.tsx', 'tsx'],
7
+ ['.js', 'js'],
8
+ ['.jsx', 'jsx'],
9
+ ])
10
+
11
+ export async function validateChangedModules(paths: string[]): Promise<void> {
12
+ const diagnostics: string[] = []
13
+ for (const filePath of paths) {
14
+ const loader = LOADERS.get(path.extname(filePath).toLowerCase())
15
+ if (!loader || !existsSync(filePath)) continue
16
+ const transpiler = new Bun.Transpiler({ loader, target: 'bun' })
17
+ try {
18
+ transpiler.transformSync(readFileSync(filePath, 'utf8'))
19
+ } catch (error) {
20
+ diagnostics.push(`${filePath}: ${diagnosticMessage(error)}`)
21
+ }
22
+ }
23
+ if (diagnostics.length > 0) {
24
+ throw new Error(`Invalid changed module syntax:\n${diagnostics.join('\n')}`)
25
+ }
26
+ }
27
+
28
+ function diagnosticMessage(error: unknown): string {
29
+ if (error && typeof error === 'object' && 'message' in error) {
30
+ return String((error as { message: unknown }).message)
31
+ }
32
+ return String(error)
33
+ }
@@ -6,6 +6,7 @@ export type ChangeKind = 'ts' | 'css' | 'component-css' | 'html' | 'islands' | '
6
6
  const IGNORE_DIR_SEGMENTS = new Set(['node_modules', '.git', '.brust', 'dist'])
7
7
  const TS_RE = /\.(tsx?|jsx?)$/
8
8
  const TEST_RE = /\.test\.(tsx?|jsx?)$/
9
+ const KIND_PRIORITY: ChangeKind[] = ['islands', 'ts', 'md', 'html', 'css', 'component-css']
9
10
 
10
11
  /** Classify a changed path. Returns null when the path should be ignored.
11
12
  * `root` is used to compute the relative path for ignore-segment matching.
@@ -83,26 +84,38 @@ export interface Watcher {
83
84
  close(): void
84
85
  }
85
86
 
86
- /** Watch `root` recursively. Emits one `onChange` call per debounce window
87
- * with paths classified by the dominant kind. Mixed-kind windows pick
88
- * by priority: islands > ts > html > css (islands trigger a full restart
89
- * that subsumes the others). */
87
+ /** Internal exposed for deterministic callback-contract tests. */
88
+ export function _testDispatchChanges(
89
+ paths: string[],
90
+ opts: Pick<CreateWatcherOptions, 'root' | 'hasMdRoutes' | 'onChange'>,
91
+ ): void {
92
+ const grouped = new Map<ChangeKind, Set<string>>()
93
+ for (const p of paths) {
94
+ const kind = classifyPath(p, opts.root, opts.hasMdRoutes ?? true)
95
+ if (kind === null) continue
96
+ let group = grouped.get(kind)
97
+ if (!group) {
98
+ group = new Set()
99
+ grouped.set(kind, group)
100
+ }
101
+ group.add(p)
102
+ }
103
+ for (const kind of KIND_PRIORITY) {
104
+ const group = grouped.get(kind)
105
+ if (group && group.size > 0) {
106
+ opts.onChange({ paths: Array.from(group), kind })
107
+ }
108
+ }
109
+ }
110
+
111
+ /** Watch `root` recursively. Emits one `onChange` call for every distinct kind
112
+ * retained in a debounce window. Priority orders delivery; it never discards
113
+ * lower-priority kinds from a mixed window. */
90
114
  export function createWatcher(opts: CreateWatcherOptions): Watcher {
91
115
  const debounceMs = opts.debounceMs ?? 50
92
- const kindPriority: ChangeKind[] = ['islands', 'ts', 'md', 'html', 'css', 'component-css']
93
116
 
94
117
  const coalesce = _testCoalesce(debounceMs, (paths) => {
95
- const kinds = new Set<ChangeKind>()
96
- const keep: string[] = []
97
- for (const p of paths) {
98
- const k = classifyPath(p, opts.root, opts.hasMdRoutes ?? true)
99
- if (k === null) continue
100
- kinds.add(k)
101
- keep.push(p)
102
- }
103
- if (keep.length === 0) return
104
- const dominant = kindPriority.find((k) => kinds.has(k))!
105
- opts.onChange({ paths: keep, kind: dominant })
118
+ _testDispatchChanges(paths, opts)
106
119
  })
107
120
 
108
121
  const fsWatcher: FSWatcher = watch(opts.root, { recursive: true }, (_event, filename) => {
package/runtime/index.js CHANGED
@@ -77,8 +77,8 @@ function requireNative() {
77
77
  try {
78
78
  const binding = require('brustjs-android-arm64')
79
79
  const bindingPackageVersion = require('brustjs-android-arm64/package.json').version
80
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
81
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
80
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
81
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
82
82
  }
83
83
  return binding
84
84
  } catch (e) {
@@ -93,8 +93,8 @@ function requireNative() {
93
93
  try {
94
94
  const binding = require('brustjs-android-arm-eabi')
95
95
  const bindingPackageVersion = require('brustjs-android-arm-eabi/package.json').version
96
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
97
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
96
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
97
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
98
98
  }
99
99
  return binding
100
100
  } catch (e) {
@@ -114,8 +114,8 @@ function requireNative() {
114
114
  try {
115
115
  const binding = require('brustjs-win32-x64-gnu')
116
116
  const bindingPackageVersion = require('brustjs-win32-x64-gnu/package.json').version
117
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
118
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
117
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
118
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
119
119
  }
120
120
  return binding
121
121
  } catch (e) {
@@ -130,8 +130,8 @@ function requireNative() {
130
130
  try {
131
131
  const binding = require('brustjs-win32-x64-msvc')
132
132
  const bindingPackageVersion = require('brustjs-win32-x64-msvc/package.json').version
133
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
134
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
133
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
134
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
135
135
  }
136
136
  return binding
137
137
  } catch (e) {
@@ -147,8 +147,8 @@ function requireNative() {
147
147
  try {
148
148
  const binding = require('brustjs-win32-ia32-msvc')
149
149
  const bindingPackageVersion = require('brustjs-win32-ia32-msvc/package.json').version
150
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
151
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
150
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
151
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
152
152
  }
153
153
  return binding
154
154
  } catch (e) {
@@ -163,8 +163,8 @@ function requireNative() {
163
163
  try {
164
164
  const binding = require('brustjs-win32-arm64-msvc')
165
165
  const bindingPackageVersion = require('brustjs-win32-arm64-msvc/package.json').version
166
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
167
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
166
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
167
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
168
168
  }
169
169
  return binding
170
170
  } catch (e) {
@@ -182,8 +182,8 @@ function requireNative() {
182
182
  try {
183
183
  const binding = require('brustjs-darwin-universal')
184
184
  const bindingPackageVersion = require('brustjs-darwin-universal/package.json').version
185
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
186
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
185
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
186
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
187
187
  }
188
188
  return binding
189
189
  } catch (e) {
@@ -198,8 +198,8 @@ function requireNative() {
198
198
  try {
199
199
  const binding = require('brustjs-darwin-x64')
200
200
  const bindingPackageVersion = require('brustjs-darwin-x64/package.json').version
201
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
202
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
201
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
202
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
203
203
  }
204
204
  return binding
205
205
  } catch (e) {
@@ -214,8 +214,8 @@ function requireNative() {
214
214
  try {
215
215
  const binding = require('brustjs-darwin-arm64')
216
216
  const bindingPackageVersion = require('brustjs-darwin-arm64/package.json').version
217
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
218
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
217
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
218
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
219
219
  }
220
220
  return binding
221
221
  } catch (e) {
@@ -234,8 +234,8 @@ function requireNative() {
234
234
  try {
235
235
  const binding = require('brustjs-freebsd-x64')
236
236
  const bindingPackageVersion = require('brustjs-freebsd-x64/package.json').version
237
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
238
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
237
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
238
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
239
239
  }
240
240
  return binding
241
241
  } catch (e) {
@@ -250,8 +250,8 @@ function requireNative() {
250
250
  try {
251
251
  const binding = require('brustjs-freebsd-arm64')
252
252
  const bindingPackageVersion = require('brustjs-freebsd-arm64/package.json').version
253
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
254
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
253
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
254
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
255
255
  }
256
256
  return binding
257
257
  } catch (e) {
@@ -271,8 +271,8 @@ function requireNative() {
271
271
  try {
272
272
  const binding = require('brustjs-linux-x64-musl')
273
273
  const bindingPackageVersion = require('brustjs-linux-x64-musl/package.json').version
274
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
275
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
274
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
275
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
276
276
  }
277
277
  return binding
278
278
  } catch (e) {
@@ -287,8 +287,8 @@ function requireNative() {
287
287
  try {
288
288
  const binding = require('brustjs-linux-x64-gnu')
289
289
  const bindingPackageVersion = require('brustjs-linux-x64-gnu/package.json').version
290
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
291
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
290
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
291
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
292
292
  }
293
293
  return binding
294
294
  } catch (e) {
@@ -305,8 +305,8 @@ function requireNative() {
305
305
  try {
306
306
  const binding = require('brustjs-linux-arm64-musl')
307
307
  const bindingPackageVersion = require('brustjs-linux-arm64-musl/package.json').version
308
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
309
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
308
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
309
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
310
310
  }
311
311
  return binding
312
312
  } catch (e) {
@@ -321,8 +321,8 @@ function requireNative() {
321
321
  try {
322
322
  const binding = require('brustjs-linux-arm64-gnu')
323
323
  const bindingPackageVersion = require('brustjs-linux-arm64-gnu/package.json').version
324
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
325
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
324
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
325
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
326
326
  }
327
327
  return binding
328
328
  } catch (e) {
@@ -339,8 +339,8 @@ function requireNative() {
339
339
  try {
340
340
  const binding = require('brustjs-linux-arm-musleabihf')
341
341
  const bindingPackageVersion = require('brustjs-linux-arm-musleabihf/package.json').version
342
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
343
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
342
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
343
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
344
344
  }
345
345
  return binding
346
346
  } catch (e) {
@@ -355,8 +355,8 @@ function requireNative() {
355
355
  try {
356
356
  const binding = require('brustjs-linux-arm-gnueabihf')
357
357
  const bindingPackageVersion = require('brustjs-linux-arm-gnueabihf/package.json').version
358
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
359
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
358
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
359
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
360
360
  }
361
361
  return binding
362
362
  } catch (e) {
@@ -373,8 +373,8 @@ function requireNative() {
373
373
  try {
374
374
  const binding = require('brustjs-linux-loong64-musl')
375
375
  const bindingPackageVersion = require('brustjs-linux-loong64-musl/package.json').version
376
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
377
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
376
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
377
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
378
378
  }
379
379
  return binding
380
380
  } catch (e) {
@@ -389,8 +389,8 @@ function requireNative() {
389
389
  try {
390
390
  const binding = require('brustjs-linux-loong64-gnu')
391
391
  const bindingPackageVersion = require('brustjs-linux-loong64-gnu/package.json').version
392
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
393
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
392
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
393
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
394
394
  }
395
395
  return binding
396
396
  } catch (e) {
@@ -407,8 +407,8 @@ function requireNative() {
407
407
  try {
408
408
  const binding = require('brustjs-linux-riscv64-musl')
409
409
  const bindingPackageVersion = require('brustjs-linux-riscv64-musl/package.json').version
410
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
411
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
410
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
411
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
412
412
  }
413
413
  return binding
414
414
  } catch (e) {
@@ -423,8 +423,8 @@ function requireNative() {
423
423
  try {
424
424
  const binding = require('brustjs-linux-riscv64-gnu')
425
425
  const bindingPackageVersion = require('brustjs-linux-riscv64-gnu/package.json').version
426
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
427
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
426
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
427
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
428
428
  }
429
429
  return binding
430
430
  } catch (e) {
@@ -440,8 +440,8 @@ function requireNative() {
440
440
  try {
441
441
  const binding = require('brustjs-linux-ppc64-gnu')
442
442
  const bindingPackageVersion = require('brustjs-linux-ppc64-gnu/package.json').version
443
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
444
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
443
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
444
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
445
445
  }
446
446
  return binding
447
447
  } catch (e) {
@@ -456,8 +456,8 @@ function requireNative() {
456
456
  try {
457
457
  const binding = require('brustjs-linux-s390x-gnu')
458
458
  const bindingPackageVersion = require('brustjs-linux-s390x-gnu/package.json').version
459
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
460
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
459
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
460
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
461
461
  }
462
462
  return binding
463
463
  } catch (e) {
@@ -476,8 +476,8 @@ function requireNative() {
476
476
  try {
477
477
  const binding = require('brustjs-openharmony-arm64')
478
478
  const bindingPackageVersion = require('brustjs-openharmony-arm64/package.json').version
479
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
480
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
479
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
480
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
481
481
  }
482
482
  return binding
483
483
  } catch (e) {
@@ -492,8 +492,8 @@ function requireNative() {
492
492
  try {
493
493
  const binding = require('brustjs-openharmony-x64')
494
494
  const bindingPackageVersion = require('brustjs-openharmony-x64/package.json').version
495
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
496
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
495
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
496
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
497
497
  }
498
498
  return binding
499
499
  } catch (e) {
@@ -508,8 +508,8 @@ function requireNative() {
508
508
  try {
509
509
  const binding = require('brustjs-openharmony-arm')
510
510
  const bindingPackageVersion = require('brustjs-openharmony-arm/package.json').version
511
- if (bindingPackageVersion !== '0.1.65-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
512
- throw new Error(`Native binding package version mismatch, expected 0.1.65-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
511
+ if (bindingPackageVersion !== '0.1.67-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
512
+ throw new Error(`Native binding package version mismatch, expected 0.1.67-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
513
513
  }
514
514
  return binding
515
515
  } catch (e) {
package/runtime/index.ts CHANGED
@@ -869,6 +869,7 @@ export const brust = {
869
869
  ;(native as any).configureDevMode?.(true)
870
870
  const { createWatcher } = await import('./dev/watcher.ts')
871
871
  const { Coordinator } = await import('./dev/coordinator.ts')
872
+ const { validateChangedModules } = await import('./dev/validate-change.ts')
872
873
  const { broadcast } = await import('./dev/ws-channel.ts')
873
874
  const { Tui } = await import('./dev/tui.ts')
874
875
  const { terminateAll: termWorkers, spawnAll: spawnWorkers } = await import(
@@ -909,6 +910,7 @@ export const brust = {
909
910
  },
910
911
  },
911
912
  reEmitJinja,
913
+ validateChanges: validateChangedModules,
912
914
  // Wipe the Rust-side island ISR cache on every render-affecting reload
913
915
  // so a frozen island render never survives a `.tsx` edit in dev.
914
916
  clearIslandCache: () => {
@@ -66,6 +66,7 @@ export interface RenderBranchStreamingArgs {
66
66
  }
67
67
 
68
68
  const encoder = new TextEncoder()
69
+ const decoder = new TextDecoder()
69
70
 
70
71
  /** JSON.stringify the per-chunk meta. Defaults match the renderToString
71
72
  * path so single-chunk responses keep their existing wire shape. */
@@ -122,6 +123,18 @@ function concatBuffers(parts: Uint8Array[], withBootstrap: boolean): Uint8Array
122
123
  return out
123
124
  }
124
125
 
126
+ function injectOrPrependDevClient(body: Uint8Array, snippet: string | null): Uint8Array {
127
+ if (!snippet) return body
128
+ if (decoder.decode(body).toLowerCase().includes('</head>')) {
129
+ return injectDevClient(body, snippet)
130
+ }
131
+ const snippetBytes = encoder.encode(snippet)
132
+ const out = new Uint8Array(snippetBytes.length + body.length)
133
+ out.set(snippetBytes, 0)
134
+ out.set(body, snippetBytes.length)
135
+ return out
136
+ }
137
+
125
138
  export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<void> {
126
139
  const { element, view, workerId, napi, errorBoundary } = args
127
140
  const slot = args.slot ?? 0
@@ -304,11 +317,12 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
304
317
  onShellError(err) {
305
318
  try {
306
319
  const html = renderToString(createElement(errorBoundary, { error: err as Error }))
320
+ const body = injectOrPrependDevClient(encoder.encode(html), getDevClientSnippet())
307
321
  const meta = makeMeta({ status: 500, streaming: false })
308
322
  mode = 'done'
309
323
  ;(async () => {
310
324
  try {
311
- const len = encodeFirstChunk(view, meta, encoder.encode(html))
325
+ const len = encodeFirstChunk(view, meta, body)
312
326
  await napi.renderChunkFinal(workerId, slot, len, view)
313
327
  finalSent = true
314
328
  resolve()
@@ -318,6 +332,23 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
318
332
  })()
319
333
  } catch (e2) {
320
334
  console.error('[brust] errorBoundary threw during shell error:', e2)
335
+ const devSnippet = getDevClientSnippet()
336
+ if (devSnippet) {
337
+ const meta = makeMeta({ status: 500, streaming: false })
338
+ const html = `<!doctype html><html><head>${devSnippet}</head><body>Internal Server Error</body></html>`
339
+ mode = 'done'
340
+ ;(async () => {
341
+ try {
342
+ const len = encodeFirstChunk(view, meta, encoder.encode(html))
343
+ await napi.renderChunkFinal(workerId, slot, len, view)
344
+ finalSent = true
345
+ resolve()
346
+ } catch (e) {
347
+ reject(e)
348
+ }
349
+ })()
350
+ return
351
+ }
321
352
  const meta = makeMeta({
322
353
  status: 500,
323
354
  streaming: false,
@@ -71,11 +71,8 @@ export declare function countMainTags(template: string): number;
71
71
  *
72
72
  * Exported for the md emit step (runtime/md/emit.ts), which bakes the same tag
73
73
  * under its `withDevClient` option — md pages render Rust-side too, so without
74
- * it they never auto-reload in dev.
75
- *
76
- * The AI runtime script is injected here as well when BRUST_AI=1. The tag is
77
- * document-only: the compiler emits a head anchor for full documents, while
78
- * fragment templates (no head) are left unchanged. */
74
+ * it they never auto-reload in dev. AI injection is deliberately separate —
75
+ * production AI must never pull in this reconnect/overlay client. */
79
76
  export declare function injectDevClientIntoTemplate(template: string): string;
80
77
  /** Bake the directive runtime loader into a native template iff it uses any
81
78
  * x-data directive. Idempotent. Wrapped in {% raw %} for symmetry with the islands
@@ -7,6 +7,8 @@ export interface CoordinatorDeps {
7
7
  };
8
8
  buildCss: () => Promise<void>;
9
9
  buildIslands: () => Promise<void>;
10
+ /** Parse changed JS/TS modules before mutating the live generation. */
11
+ validateChanges: (paths: string[]) => Promise<void>;
10
12
  /** Recompile native-route `.jinja` templates from source and reload them into
11
13
  * the minijinja env, so `native: true` routes pick up .tsx edits on reload. */
12
14
  reEmitJinja: () => Promise<void>;
@@ -24,10 +26,15 @@ export interface CoordinatorDeps {
24
26
  }
25
27
  export declare class Coordinator {
26
28
  private deps;
27
- private state;
29
+ private readonly pending;
30
+ private drainPromise;
28
31
  constructor(deps: CoordinatorDeps);
29
32
  handleChange(ev: {
30
33
  paths: string[];
31
34
  kind: ChangeKind;
32
35
  }): Promise<void>;
36
+ private startDrain;
37
+ private drainPending;
38
+ private takeNext;
39
+ private runBatch;
33
40
  }
@@ -0,0 +1 @@
1
+ export declare function validateChangedModules(paths: string[]): Promise<void>;
@@ -26,9 +26,10 @@ export interface CreateWatcherOptions {
26
26
  export interface Watcher {
27
27
  close(): void;
28
28
  }
29
- /** Watch `root` recursively. Emits one `onChange` call per debounce window
30
- * with paths classified by the dominant kind. Mixed-kind windows pick
31
- * by priority: islands > ts > html > css (islands trigger a full restart
32
- * that subsumes the others). */
29
+ /** Internal exposed for deterministic callback-contract tests. */
30
+ export declare function _testDispatchChanges(paths: string[], opts: Pick<CreateWatcherOptions, 'root' | 'hasMdRoutes' | 'onChange'>): void;
31
+ /** Watch `root` recursively. Emits one `onChange` call for every distinct kind
32
+ * retained in a debounce window. Priority orders delivery; it never discards
33
+ * lower-priority kinds from a mixed window. */
33
34
  export declare function createWatcher(opts: CreateWatcherOptions): Watcher;
34
35
  export {};