@christianirack/brainllm-runtime 0.10.4 → 0.10.11

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/brain-env.js CHANGED
@@ -143,8 +143,8 @@ const strip = p => {
143
143
  const m = G.document && document.querySelector('meta[name=brain-base]'), b = (m && m.getAttribute('content')) || '/'
144
144
  return b !== '/' && (p + '/').startsWith(b) ? '/' + p.slice(b.length) : p
145
145
  }
146
- const read = () => {
147
- const u = new URL(location.href)
146
+ const read = (href = location.href) => {
147
+ const u = new URL(href, location.href)
148
148
  return [strip(u.pathname), paramsOf(), Object.fromEntries(u.searchParams)]
149
149
  }
150
150
  export function route() {
@@ -154,8 +154,10 @@ export function route() {
154
154
  const S = [...read(), false].map(v => sig(v))
155
155
  cur = { get path() { return S[0].v }, get params() { return S[1].v }, get query() { return S[2].v }, get navigating() { return S[3].v } }
156
156
  G.__brainRouted = on => {
157
- if (on !== undefined) return void (S[3].v = !!on)
158
- const v = read()
157
+ // A string is the destination URL: the router passes it before the new view mounts, since the Navigation API may
158
+ // not have committed location yet and the view would read the previous page's query.
159
+ if (on < 2) return void (S[3].v = !!on) // 0|1|boolean: the navigating flag; a string is a URL
160
+ const v = read(on)
159
161
  v.forEach((x, i) => JSON.stringify(untrack(() => S[i].v)) !== JSON.stringify(x) && (S[i].v = x)) // only what changed
160
162
  }
161
163
  }
package/brain-router.js CHANGED
@@ -47,10 +47,11 @@ G.__brainNavigate = navigate // brain-form follows a server function's { redirec
47
47
 
48
48
  // route() lives in brain-env.js (it needs the core's signals; a page without islands downloads only this router):
49
49
  // after every client navigation the router calls the hook it installs.
50
- const routed = () => G.__brainRouted?.()
50
+ const routed = to => G.__brainRouted?.(to)
51
51
 
52
52
  // Active links (§41.5): every internal <a href> to the current page gets aria-current="page" (the CSS hook is
53
53
  // [aria-current=page]); the others lose it. On start and after every client navigation.
54
+ const marked = new WeakSet()
54
55
  const current = () => {
55
56
  if (!G.document || !G.location) return
56
57
  for (const a of document.querySelectorAll('a[href]')) {
@@ -60,8 +61,11 @@ const current = () => {
60
61
  } catch {
61
62
  continue
62
63
  }
63
- if (u.origin === location.origin && u.pathname.replace(/\/$/, '') === location.pathname.replace(/\/$/, '')) a.setAttribute('aria-current', 'page')
64
- else if (a.getAttribute('aria-current') === 'page') a.removeAttribute('aria-current')
64
+ // An aria-current the author wrote or bound is theirs: the router only manages the ones it set itself.
65
+ if (a.hasAttribute('aria-current') && !marked.has(a)) continue
66
+ // A link with its own query is current only on that exact query (`/?tab=b` is not the page `/?tab=a`).
67
+ if (u.origin === location.origin && u.pathname.replace(/\/$/, '') === location.pathname.replace(/\/$/, '') && (!u.search || u.search === location.search)) a.setAttribute('aria-current', 'page'), marked.add(a)
68
+ else if (marked.delete(a)) a.removeAttribute('aria-current')
65
69
  }
66
70
  }
67
71
 
@@ -217,8 +221,10 @@ export function router(o = {}) {
217
221
  const cur = document.querySelector(region), next = doc.querySelector(region)
218
222
  if (!cur || !next) return hard(url)
219
223
  // DOM, head and active links change in one callback (one frame); a newer navigation that started meanwhile skips it
224
+ const dest = r.redirected && r.url || url
220
225
  const swap = () => {
221
226
  if (my !== seq) return
227
+ routed(dest) // route() already shows the destination when the new view mounts
222
228
  dispose(cur)
223
229
  fresh()
224
230
  cur.replaceChildren(...[...next.childNodes].map(adopt))
@@ -320,21 +326,25 @@ export function router(o = {}) {
320
326
  const shown = u => load(u).then(ok => ok && landed(u)) // the browser restores scroll; focus is ours (it would reset to body)
321
327
  if (!gs.length) return e.intercept({ focusReset: 'manual', handler: () => shown(to) })
322
328
  let g
323
- e.intercept({
329
+ const opts = {
324
330
  focusReset: 'manual',
325
- // Chrome 141+: decided before the URL commits (cancel = the address bar never changes; native redirect).
326
- async precommitHandler(c) {
327
- g = await check(to)
328
- if (typeof g === 'string' && c.redirect) c.redirect(new URL(app(g), location.href).href), (g = true)
329
- else if (g === false) throw new DOMException('guard', 'AbortError')
330
- },
331
331
  async handler() {
332
332
  if (g === undefined) g = await check(to) // no precommit support: the URL already committed
333
333
  if (g === true) return shown(nav.currentEntry ? nav.currentEntry.url : to)
334
334
  if (g === null) return hard(to)
335
335
  nav.navigate(g === false ? from : new URL(app(g), location.href).href, { history: 'replace' })
336
336
  },
337
- })
337
+ }
338
+ // Chrome 141+: decided before the URL commits (cancel = the address bar never changes; native redirect).
339
+ // Only on cancelable events: a back/forward traverse is not cancelable and intercept() throws with a
340
+ // precommitHandler there, leaving the old view on screen; the handler then decides after the commit.
341
+ if (e.cancelable)
342
+ opts.precommitHandler = async c => {
343
+ g = await check(to)
344
+ if (typeof g === 'string' && c.redirect) c.redirect(new URL(app(g), location.href).href), (g = true)
345
+ else if (g === false) throw new DOMException('guard', 'AbortError')
346
+ }
347
+ e.intercept(opts)
338
348
  }
339
349
  nav.addEventListener('navigate', h)
340
350
  ahead.scan()
package/brain-url.js CHANGED
@@ -109,6 +109,26 @@ export function urlDialog(element, key, options) {
109
109
  }
110
110
 
111
111
  // Internal compiler binding for public select/tab/modal signals, once during component setup.
112
+ let pending = null
113
+ function flushUrl() {
114
+ const writes = pending
115
+ pending = null
116
+ if (!writes || !G.location) return
117
+ const next = new URL(G.location.href)
118
+ let replace = true
119
+ for (const [key, path, value, quiet] of writes) {
120
+ if (next.pathname !== path) continue
121
+ if (value == null) next.searchParams.delete(key)
122
+ else next.searchParams.set(key, value)
123
+ replace &&= quiet
124
+ }
125
+ if (next.search.length > 8192) { report('URL state exceeds 8 KiB; shorten public selections'); return }
126
+ if (next.href === G.location.href) return
127
+ try {
128
+ G.history[replace ? 'replaceState' : 'pushState'](G.history.state, '', next.href)
129
+ G.__brainRouted?.()
130
+ } catch { report('URL state history write failed; reduce the frequency of changes or use history: replace') }
131
+ }
112
132
  export function urlBind(cell, key, options = {}) {
113
133
  if (G.__brainRoute || G.__brainSSR) {
114
134
  if (typeof key === 'string' && key.length <= 200 && !privateKey.test(key) && encode(untrack(() => cell.v)) != null) {
@@ -141,16 +161,10 @@ export function urlBind(cell, key, options = {}) {
141
161
  if (value === b.last) return
142
162
  b.last = value
143
163
  if (value == null) { report('URL state value is private, unsupported or too large; keep it out of navigation state'); return }
144
- const next = new URL(G.location.href)
145
- if (next.pathname !== b.path) return
146
- if (value === encode(base)) next.searchParams.delete(key)
147
- else next.searchParams.set(key, value)
148
- if (next.search.length > 8192) { report('URL state exceeds 8 KiB; shorten public selections'); return }
149
- if (next.href === G.location.href) return
150
- try {
151
- G.history[options.history === 'replace' ? 'replaceState' : 'pushState'](G.history.state, '', next.href)
152
- G.__brainRouted?.()
153
- } catch { report('URL state history write failed; reduce the frequency of changes or use history: replace') }
164
+ // Several keys written in one handler land in one history entry: writing each at once let the first write's
165
+ // route notification restore the others from the old URL before their own effect ran.
166
+ if (!pending) { pending = []; queueMicrotask(flushUrl) }
167
+ pending.push([key, b.path, value === encode(base) ? null : value, options.history === 'replace'])
154
168
  })
155
169
  fx(() => () => {
156
170
  if (bindings.get(key) === b) bindings.delete(key)
package/brain.js CHANGED
@@ -246,13 +246,14 @@ const bind = (f, s) => {
246
246
  })
247
247
  }
248
248
 
249
- export function tpl(h) {
249
+ export function tpl(h, s) {
250
250
  let n
251
251
  return () => {
252
252
  if (!n) {
253
- const t = document.createElement('template')
253
+ const t = document.createElement('template'), c = t.content
254
254
  t.innerHTML = h
255
- const c = t.content
255
+ // s: the compiler wrapped an SVG block fragment in <svg> so it parses in the SVG namespace; unwrap it
256
+ if (s) c.replaceChildren(...c.firstChild.childNodes)
256
257
  n = c.childNodes.length == 1 ? c.firstChild : c
257
258
  }
258
259
  // A: SSR stamps the clone, hydration adopts the node the server rendered (#285)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@christianirack/brainllm-runtime",
3
- "version": "0.10.4",
3
+ "version": "0.10.11",
4
4
  "description": "The BRAINLLM runtime for the Rust-built web compiler for LLMs: dependency-free ES modules (signals, DOM, router, data, forms, channels). The brain binary embeds this same version.",
5
5
  "type": "module",
6
6
  "exports": {
package/server/server.mjs CHANGED
@@ -100,14 +100,15 @@ function intendedStatus(error) {
100
100
  return Number.isInteger(status) && STATUS_CODES[status] && (error instanceof HttpError && status >= 400 && status <= 599 || plain && status >= 400 && status < 500) ? status : null
101
101
  }
102
102
  // A deliberate public failure → { status, message } for f.error: a plain `{ message }` without a status (400) or an
103
- // author's Error subclass with a 4xx `status`. Plain `{ status, message }` keeps its message private (driver detail);
103
+ // author's Error subclass with a 4xx `status`, or `throw { status: 404, message, public: true }` (BrainScript has no
104
+ // `class`). Plain `{ status, message }` without `public` keeps its message private (driver detail);
104
105
  // built-in errors and bare `Error` messages stay private too.
105
106
  const BUILTIN_ERRORS = new Set([Error, TypeError, RangeError, SyntaxError, ReferenceError, EvalError, URIError, AggregateError].map(c => c.prototype))
106
107
  function publicError(error) {
107
108
  if (!error || typeof error !== 'object' || error instanceof HttpError) return null
108
109
  const proto = Object.getPrototypeOf(error), plain = proto === Object.prototype || proto === null
109
110
  const typed = error instanceof Error && !BUILTIN_ERRORS.has(proto)
110
- const status = plain ? (error.status === undefined ? 400 : 0) : error.status, message = error.message
111
+ const status = plain ? (error.status === undefined ? 400 : error.public === true ? error.status : 0) : error.status, message = error.message
111
112
  if (!(plain || typed) || typeof message !== 'string' || !message || message.length > 500) return null
112
113
  return Number.isInteger(status) && status >= 400 && status < 500 && STATUS_CODES[status] ? { status, message } : null
113
114
  }
@@ -1089,6 +1090,11 @@ export function createServer(o = {}) {
1089
1090
  try {const path=join(dist,'brain-build.json'),meta=lstatSync(path);if(meta.isFile()&&!meta.isSymbolicLink()&&meta.size<=16384&&JSON.parse(readFileSync(path,'utf8')).compilerBranding===false)compilerBranding=false}catch{}
1090
1091
  const SECURITY={...DEFAULT_SECURITY}
1091
1092
  if(!compilerBranding)delete SECURITY['x-generated-by']
1093
+ // brain.json `csp.frameAncestors` (written into the build policy) decides who may embed the pages: XFO cannot list
1094
+ // origins, so it stays DENY only while the policy forbids every embedder, SAMEORIGIN for 'self' alone.
1095
+ const ancestors = /(?:^|;)\s*frame-ancestors\s+([^;]*)/.exec(mf.csp || '')?.[1]?.trim() || "'none'"
1096
+ if (ancestors === "'self'") SECURITY['x-frame-options'] = 'SAMEORIGIN'
1097
+ else if (ancestors !== "'none'") delete SECURITY['x-frame-options']
1092
1098
  // brain.json `server` (§11.1), written into the manifest by the build; createServer options and env still win
1093
1099
  const SL = mf.server || {}
1094
1100
  const TIMEOUT = +(o.timeout ?? env.BRAIN_TIMEOUT_MS ?? SL.timeoutMs ?? 10000)
package/shim/dom.js CHANGED
@@ -181,6 +181,14 @@ class Node {
181
181
  getBoundingClientRect() {
182
182
  return { top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0 }
183
183
  }
184
+ getClientRects() { return [] }
185
+ // Layout and focus have no meaning while the server renders: an effect that calls them must not fail the build.
186
+ scrollIntoView() {}
187
+ scrollTo() {}
188
+ scrollBy() {}
189
+ focus() {}
190
+ blur() {}
191
+ select() {}
184
192
  contains(n) {
185
193
  for (; n; n = n.parentNode) if (n === this) return true
186
194
  return false