@christianirack/brainllm-runtime 0.10.6 → 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 +6 -4
- package/brain-router.js +9 -3
- package/brain-url.js +24 -10
- package/brain.js +4 -3
- package/package.json +1 -1
- package/server/server.mjs +8 -2
- package/shim/dom.js +8 -0
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
|
-
|
|
158
|
-
|
|
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 =
|
|
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
|
-
|
|
64
|
-
|
|
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))
|
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
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|