mnfst-render 0.5.22 → 0.5.23
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/manifest.render.mjs +66 -5
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -671,6 +671,51 @@ function stripDataTailwindAttr(html) {
|
|
|
671
671
|
return html.replace(/\sdata-tailwind(?:=(["']).*?\1)?/gi, '');
|
|
672
672
|
}
|
|
673
673
|
|
|
674
|
+
/** Theme class de-bake + synchronous bootstrap.
|
|
675
|
+
*
|
|
676
|
+
* Puppeteer applies `<html class="light">` or `<html class="dark">` based on
|
|
677
|
+
* the build host's system preference at prerender time. Shipping that baked
|
|
678
|
+
* class to users in the OPPOSITE preference causes a visible flash on every
|
|
679
|
+
* page load (dark→light or light→dark) until the themes plugin re-evaluates.
|
|
680
|
+
*
|
|
681
|
+
* Fix: strip `light`/`dark` from the baked `<html class>` and inject a tiny
|
|
682
|
+
* synchronous `<script>` at the top of `<head>` that sets the correct class
|
|
683
|
+
* BEFORE the first paint — based on the user's `localStorage.theme` (their
|
|
684
|
+
* saved preference) or `prefers-color-scheme` (their system preference).
|
|
685
|
+
*
|
|
686
|
+
* The themes plugin (`manifest.themes.js`) still runs later for reactivity
|
|
687
|
+
* (Alpine bindings, click handlers, system-preference change listener), but
|
|
688
|
+
* the initial paint already has the correct class so there's no flash.
|
|
689
|
+
*/
|
|
690
|
+
function debakeThemeClass(html) {
|
|
691
|
+
// Strip `light`/`dark` from `<html class="...">`. When the class attribute
|
|
692
|
+
// becomes empty, drop the attribute entirely (including its leading space)
|
|
693
|
+
// while preserving the rest of the `<html ...>` tag. Bug-fixed twice — the
|
|
694
|
+
// earlier version's regex captured the entire `<html ... class="...` chunk
|
|
695
|
+
// so returning `''` for an empty cleaned class wiped the whole opening tag.
|
|
696
|
+
let out = html.replace(/<html\b([^>]*)>/i, (full, attrs) => {
|
|
697
|
+
const newAttrs = attrs.replace(/\sclass=(["'])([^"']*)\1/i, (_, q, classes) => {
|
|
698
|
+
const cleaned = classes
|
|
699
|
+
.split(/\s+/)
|
|
700
|
+
.filter((c) => c && c !== 'light' && c !== 'dark')
|
|
701
|
+
.join(' ')
|
|
702
|
+
.trim();
|
|
703
|
+
return cleaned ? ` class=${q}${cleaned}${q}` : '';
|
|
704
|
+
});
|
|
705
|
+
return `<html${newAttrs}>`;
|
|
706
|
+
});
|
|
707
|
+
// Inject the synchronous theme bootstrap as the FIRST element inside <head>
|
|
708
|
+
// so it runs before any CSS or other scripts. Self-contained — reads
|
|
709
|
+
// localStorage + prefers-color-scheme and sets the class atomically.
|
|
710
|
+
const bootstrap = `<script>(function(){try{var t=localStorage.getItem('theme')||'system';var d=t==='dark'||(t==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.add(d?'dark':'light');}catch(e){document.documentElement.classList.add('light');}})();</script>`;
|
|
711
|
+
if (!out.includes('id="manifest-theme-bootstrap"')) {
|
|
712
|
+
// Tag the script for idempotency on rebuilds and easy debugging.
|
|
713
|
+
const tagged = bootstrap.replace('<script>', '<script id="manifest-theme-bootstrap">');
|
|
714
|
+
out = out.replace(/<head(\s[^>]*)?>/i, (m) => `${m}\n ${tagged}`);
|
|
715
|
+
}
|
|
716
|
+
return out;
|
|
717
|
+
}
|
|
718
|
+
|
|
674
719
|
/** Manifest utilities plugin: <style id="utility-styles"> and <style id="utility-styles-critical"> */
|
|
675
720
|
function extractUtilityStyleBlocks(html) {
|
|
676
721
|
const blocks = [];
|
|
@@ -1448,6 +1493,7 @@ function generateLocaleVariantHtml({
|
|
|
1448
1493
|
} else {
|
|
1449
1494
|
html = stripDataTailwindAttr(html);
|
|
1450
1495
|
}
|
|
1496
|
+
html = debakeThemeClass(html);
|
|
1451
1497
|
|
|
1452
1498
|
const pageUtilityBlocks = [];
|
|
1453
1499
|
if (bundleUtilities) {
|
|
@@ -2488,7 +2534,7 @@ async function runPrerender(config) {
|
|
|
2488
2534
|
// Runtime-only Alpine magics whose values change after the prerender
|
|
2489
2535
|
// snapshot (e.g. via media query, route change, auth state). Bindings
|
|
2490
2536
|
// referencing these must re-evaluate in the live page.
|
|
2491
|
-
const RUNTIME_MAGIC_RX =
|
|
2537
|
+
const RUNTIME_MAGIC_RX = /(?<!['"])\$(theme|locale|url|auth|search|query|toast)\b/;
|
|
2492
2538
|
|
|
2493
2539
|
const isDiffBindingAttr = (name) =>
|
|
2494
2540
|
name === ':class' || name === 'x-bind:class' ||
|
|
@@ -2564,10 +2610,24 @@ async function runPrerender(config) {
|
|
|
2564
2610
|
// the element unstyled until Alpine + async data loads catch up.
|
|
2565
2611
|
// The baked value IS the correct initial render.
|
|
2566
2612
|
if (src === null) {
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2613
|
+
// Keep baked `style` when an Alpine binding manages it.
|
|
2614
|
+
// The baked value (e.g. `mask-image: url(...)` from a `:style`
|
|
2615
|
+
// expression evaluating against $x data, or `display: none`
|
|
2616
|
+
// from `x-show`) is the correct initial render — nulling it
|
|
2617
|
+
// would flash the element while Alpine + async data catch up.
|
|
2618
|
+
// Alpine's :style/x-show handlers diff against the current
|
|
2619
|
+
// DOM correctly, so the baked value is safely toggled later.
|
|
2620
|
+
const skipStyleNull = name === 'style' &&
|
|
2621
|
+
(':style' in source || 'x-bind:style' in source || 'x-show' in source);
|
|
2622
|
+
if (skipStyleNull) continue;
|
|
2623
|
+
// NOTE: do NOT extend this skip to `class`. Alpine's
|
|
2624
|
+
// `:class="cond ? 'foo' : ''"` (string-form) treats the
|
|
2625
|
+
// pre-existing className as the immutable baseline and only
|
|
2626
|
+
// ADDS classes on top — it cannot REMOVE a baked class. If
|
|
2627
|
+
// the prerender baked `class="selected"` for the initial
|
|
2628
|
+
// tab, clicking other tabs would never strip `selected`
|
|
2629
|
+
// from the first one. Always restoring class to null lets
|
|
2630
|
+
// Alpine manage it cleanly from a blank slate.
|
|
2571
2631
|
}
|
|
2572
2632
|
attrsOut[name] = src; // may be null (means "remove this attribute")
|
|
2573
2633
|
dirty = true;
|
|
@@ -2971,6 +3031,7 @@ async function runPrerender(config) {
|
|
|
2971
3031
|
} else {
|
|
2972
3032
|
html = stripDataTailwindAttr(html);
|
|
2973
3033
|
}
|
|
3034
|
+
html = debakeThemeClass(html);
|
|
2974
3035
|
if (bundleUtilities) {
|
|
2975
3036
|
const extracted = extractUtilityStyleBlocks(html);
|
|
2976
3037
|
html = extracted.html;
|