mnfst-render 0.5.21 → 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 +90 -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 = [];
|
|
@@ -701,6 +746,7 @@ function injectBeforeHeadClose(html, snippet) {
|
|
|
701
746
|
return out.replace(/<\/head>/i, `${snippet}\n</head>`);
|
|
702
747
|
}
|
|
703
748
|
|
|
749
|
+
|
|
704
750
|
function indexHtmlUsesTailwind(rootDir) {
|
|
705
751
|
const indexPath = join(rootDir, 'index.html');
|
|
706
752
|
if (!existsSync(indexPath)) return false;
|
|
@@ -814,6 +860,29 @@ function runTailwindCliForPrerender(rootDir, outputDir, pre) {
|
|
|
814
860
|
console.error('prerender: Tailwind CLI did not produce prerender.tailwind.css');
|
|
815
861
|
return false;
|
|
816
862
|
}
|
|
863
|
+
// Strip Tailwind preflight rules that conflict with Manifest's element-level
|
|
864
|
+
// resets. Tailwind's `hr { height: 0; border-top-width: 1px }` would win on
|
|
865
|
+
// specificity over Manifest's `:where(hr) {...}` reset (same `@layer base`,
|
|
866
|
+
// higher specificity), even when Manifest CSS loads after Tailwind. Removing
|
|
867
|
+
// the specific conflicting rules here is surgical: Tailwind's other utility
|
|
868
|
+
// classes (mt-6, md:hidden, etc.) keep their normal `@layer utilities`
|
|
869
|
+
// behaviour and continue to override Manifest's `*` reset as expected.
|
|
870
|
+
try {
|
|
871
|
+
const compiled = readFileSync(outCss, 'utf8');
|
|
872
|
+
// Inside Tailwind's `@layer base { ... }` block, remove the bare `hr { ... }`
|
|
873
|
+
// declaration only. Other element resets in the same layer don't conflict
|
|
874
|
+
// with Manifest's `:where()` resets (they target other elements or rely on
|
|
875
|
+
// Manifest's resets winning later in source order at equal specificity).
|
|
876
|
+
const stripped = compiled.replace(
|
|
877
|
+
/(\s*)hr\s*\{\s*height:\s*0;\s*color:\s*inherit;\s*border-top-width:\s*1px;?\s*\}/g,
|
|
878
|
+
''
|
|
879
|
+
);
|
|
880
|
+
if (stripped !== compiled) {
|
|
881
|
+
writeFileSync(outCss, stripped, 'utf8');
|
|
882
|
+
}
|
|
883
|
+
} catch (e) {
|
|
884
|
+
console.warn('prerender: failed to strip conflicting Tailwind preflight rules:', e?.message || e);
|
|
885
|
+
}
|
|
817
886
|
process.stdout.write(`prerender: wrote ${relative(rootDir, outCss)}\n`);
|
|
818
887
|
return true;
|
|
819
888
|
}
|
|
@@ -1424,6 +1493,7 @@ function generateLocaleVariantHtml({
|
|
|
1424
1493
|
} else {
|
|
1425
1494
|
html = stripDataTailwindAttr(html);
|
|
1426
1495
|
}
|
|
1496
|
+
html = debakeThemeClass(html);
|
|
1427
1497
|
|
|
1428
1498
|
const pageUtilityBlocks = [];
|
|
1429
1499
|
if (bundleUtilities) {
|
|
@@ -2464,7 +2534,7 @@ async function runPrerender(config) {
|
|
|
2464
2534
|
// Runtime-only Alpine magics whose values change after the prerender
|
|
2465
2535
|
// snapshot (e.g. via media query, route change, auth state). Bindings
|
|
2466
2536
|
// referencing these must re-evaluate in the live page.
|
|
2467
|
-
const RUNTIME_MAGIC_RX =
|
|
2537
|
+
const RUNTIME_MAGIC_RX = /(?<!['"])\$(theme|locale|url|auth|search|query|toast)\b/;
|
|
2468
2538
|
|
|
2469
2539
|
const isDiffBindingAttr = (name) =>
|
|
2470
2540
|
name === ':class' || name === 'x-bind:class' ||
|
|
@@ -2540,10 +2610,24 @@ async function runPrerender(config) {
|
|
|
2540
2610
|
// the element unstyled until Alpine + async data loads catch up.
|
|
2541
2611
|
// The baked value IS the correct initial render.
|
|
2542
2612
|
if (src === null) {
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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.
|
|
2547
2631
|
}
|
|
2548
2632
|
attrsOut[name] = src; // may be null (means "remove this attribute")
|
|
2549
2633
|
dirty = true;
|
|
@@ -2947,6 +3031,7 @@ async function runPrerender(config) {
|
|
|
2947
3031
|
} else {
|
|
2948
3032
|
html = stripDataTailwindAttr(html);
|
|
2949
3033
|
}
|
|
3034
|
+
html = debakeThemeClass(html);
|
|
2950
3035
|
if (bundleUtilities) {
|
|
2951
3036
|
const extracted = extractUtilityStyleBlocks(html);
|
|
2952
3037
|
html = extracted.html;
|