@symbo.ls/brender 3.14.663 → 3.14.665
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/CHANGELOG.md +46 -0
- package/README.md +106 -23
- package/dist/esm/env.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/load.js +2 -1
- package/dist/esm/prefetch.js +1 -1
- package/dist/esm/render.js +33 -51
- package/dist/esm/shell.js +9 -8
- package/env.js +8 -2
- package/index.js +5 -3
- package/load.js +111 -22
- package/package.json +9 -9
- package/prefetch.js +30 -62
- package/render.js +533 -658
- package/shell.js +74 -12
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>
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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, '"')}"`
|
|
159
|
+
})
|
|
160
|
+
.join('')
|
|
161
|
+
const href = src.replace(/"/g, '"')
|
|
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, '"')
|
|
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] -
|
|
133
|
-
*
|
|
134
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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)
|