@symbo.ls/brender 3.14.664 → 3.14.666

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/shell.js CHANGED
@@ -14,11 +14,14 @@
14
14
  * render produced replaces the shell's tag with the same identity —
15
15
  * never a second <title>), then the shell's own head, then the
16
16
  * render's font links and <style> blocks just before </head>.
17
- * <body> `window.__BRENDER__=true` and the rendered markup right after
18
- * <body>, BEFORE the shell's scripts. The client's createDomqlElement
19
- * sees the flag, removes the server markup and its <style> blocks,
20
- * and renders the app over the same body (packages/smbls
21
- * createDomql.js fallbackRender) — no duplicated tree.
17
+ * <body> the rendered markup right after <body>, then the page's data
18
+ * blocks (the element-path registry, the recorded answers), BEFORE
19
+ * the shell's scripts. No inline script: the page's mark is a
20
+ * `<meta name="brender">` the caller puts in `headTags`, so a strict
21
+ * Content-Security-Policy cannot hide the pre-render from the
22
+ * client (XMA report, brender item 1). The client's
23
+ * createDomqlElement adopts the markup, or replaces it with a fresh
24
+ * render (packages/smbls src/prerender.js) — never a second tree.
22
25
  *
23
26
  * Relative asset URLs (`src="./index.js"`, `href="style.css"`) on the shell's
24
27
  * <script>/<link> tags are rebased by `depth` directories, so a route written
@@ -122,6 +125,59 @@ const rebaseAssets = (html, depth) => {
122
125
  )
123
126
  }
124
127
 
128
+ // ── The app's own scripts, fetched first ───────────────────────────────────
129
+ // The shell's scripts sit after the pre-rendered markup (parcel keeps them at
130
+ // the end of <body>), so every image in that markup was discovered and
131
+ // requested first, and on a slow connection they shared the bandwidth: the
132
+ // app's 214 KB script finished at 2.65 s instead of 1.75 s on a throttled
133
+ // phone (xma.info report, brender build item 14). Each external script the
134
+ // app boots from gets a high-priority preload in <head> — `modulepreload` for
135
+ // a module, `preload as=script` for a classic one — with the same URL and
136
+ // CORS/integrity attributes, so the browser reuses the response. An `async`
137
+ // script (a helper the app does not wait for) or a `nomodule` one is left
138
+ // alone, as is anything that is not JavaScript (importmap, JSON data).
139
+ const SCRIPT_TYPES = new Set(['', 'module', 'text/javascript', 'application/javascript'])
140
+
141
+ const hasAttr = (tag, name) => new RegExp(`\\s${name}(?:\\s*=|\\s|>|/)`, 'i').test(tag)
142
+
143
+ export const scriptPreloads = (html) => {
144
+ const links = []
145
+ const seen = new Set()
146
+ for (const [, , raw] of rawRanges(html)) {
147
+ if (!/^<script\b/i.test(raw)) continue
148
+ const open = raw.slice(0, raw.indexOf('>') + 1)
149
+ const src = attr(open, 'src')
150
+ if (!src || seen.has(src)) continue
151
+ const type = (attr(open, 'type') || '').trim().toLowerCase()
152
+ if (!SCRIPT_TYPES.has(type) || hasAttr(open, 'async') || hasAttr(open, 'nomodule')) continue
153
+ seen.add(src)
154
+ const extra = ['crossorigin', 'integrity', 'referrerpolicy']
155
+ .map((name) => {
156
+ const value = attr(open, name)
157
+ if (value === null) return hasAttr(open, name) ? ` ${name}` : ''
158
+ return ` ${name}="${value.replace(/"/g, '&quot;')}"`
159
+ })
160
+ .join('')
161
+ const href = src.replace(/"/g, '&quot;')
162
+ links.push(type === 'module'
163
+ ? `<link rel="modulepreload" href="${href}" fetchpriority="high"${extra}>`
164
+ : `<link rel="preload" as="script" href="${href}" fetchpriority="high"${extra}>`)
165
+ }
166
+ return links.join('\n')
167
+ }
168
+
169
+ // `<html lang>` set to the page's language (kept when it already says so).
170
+ const setHtmlLang = (html, lang) => {
171
+ const htmlOpen = findOutside(html, /<html(\s[^>]*)?>/i)
172
+ if (!htmlOpen) return html
173
+ const tag = htmlOpen[0]
174
+ const value = String(lang).replace(/"/g, '&quot;')
175
+ const next = attr(tag, 'lang') !== null
176
+ ? tag.replace(/(\slang\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s>]+)/i, `$1"${value}"`)
177
+ : tag.replace(/^<html/i, `<html lang="${value}"`)
178
+ return html.slice(0, htmlOpen.index) + next + html.slice(htmlOpen.index + tag.length)
179
+ }
180
+
125
181
  /**
126
182
  * @param {string} shell - the built app document (e.g. dist/index.html)
127
183
  * @param {object} parts
@@ -129,16 +185,22 @@ const rebaseAssets = (html, depth) => {
129
185
  * @param {string} [parts.headEnd] - font links + <style> blocks for </head>
130
186
  * @param {string} [parts.body] - rendered body markup
131
187
  * @param {number} [parts.depth=0] - directory depth of the output file
132
- * @param {string} [parts.bootScripts] - inline scripts placed right after the
133
- * `__BRENDER__` flag, before the markup (the opt-in hydration registry and
134
- * fetch seed — renderPage `adoptDom`)
188
+ * @param {string} [parts.bootScripts] - data blocks placed right after the
189
+ * markup (the hydration registry and the fetch seed — renderPage
190
+ * prerenderParts)
191
+ * @param {string} [parts.lang] - the page's language, written to `<html lang>`
192
+ * @param {boolean} [parts.preloadScripts=false] - preload the shell's own
193
+ * scripts with high priority (see scriptPreloads)
135
194
  * @returns {string}
136
195
  */
137
- export const composeIntoShell = (shell, { headTags = '', headEnd = '', body = '', depth = 0, bootScripts = '' } = {}) => {
196
+ export const composeIntoShell = (shell, { headTags = '', headEnd = '', body = '', depth = 0, bootScripts = '', lang, preloadScripts = false } = {}) => {
138
197
  if (typeof shell !== 'string' || !shell.trim()) {
139
198
  throw new Error('brender: the shell document is empty')
140
199
  }
141
200
  let html = rebaseAssets(shell, depth)
201
+ if (lang) html = setHtmlLang(html, lang)
202
+ const preloads = preloadScripts ? scriptPreloads(html) : ''
203
+ if (preloads) headEnd = headEnd ? `${preloads}\n${headEnd}` : preloads
142
204
 
143
205
  const owned = new Set()
144
206
  for (const m of headTags.matchAll(HEAD_TAG)) {
@@ -170,17 +232,17 @@ export const composeIntoShell = (shell, { headTags = '', headEnd = '', body = ''
170
232
  html = html.slice(0, at) + headIn + headOut + html.slice(at)
171
233
  }
172
234
 
173
- const flagged = `\n<script>window.__BRENDER__=true</script>\n${bootScripts ? bootScripts + '\n' : ''}${body}\n`
235
+ const flagged = `\n${body}\n${bootScripts ? bootScripts + '\n' : ''}`
174
236
  const bodyOpen = findOutside(html, BODY_OPEN)
175
237
  if (bodyOpen) {
176
238
  // Right after <body>: before every shell script, so a classic inline
177
- // boot script (symbols-runner) already finds the flag and the markup.
239
+ // boot script (symbols-runner) already finds the markup and its data.
178
240
  const at = bodyOpen.index + bodyOpen[0].length
179
241
  return html.slice(0, at) + flagged + html.slice(at)
180
242
  }
181
243
  // No <body> tag: the markup opens the implied body at the end of the
182
244
  // document. Such shells load the app with module scripts (parcel, vite),
183
- // which run after parsing — the flag and the markup are in place by then.
245
+ // which run after parsing — the markup and its data are in place by then.
184
246
  const htmlClose = findOutside(html, /<\/(?:body|html)\s*>/i)
185
247
  const at = htmlClose ? htmlClose.index : html.length
186
248
  return html.slice(0, at) + flagged + html.slice(at)