UNPKG

@pinegrow/piny-astro

Version:

An Astro plugin that implements Piny integration in dev mode.

145 lines (124 loc) 5.12 kB
/// <reference types="vite/client" /> // Based on https://github.com/bluwy/astro-pages-hmr/blob/master/src/hmr-runtime.js import * as micromorph from 'micromorph'; /** @type {DOMParser | undefined} */ let parser; if (import.meta.hot) { /* listen for the custom event sent by our dev-server plugin */ import.meta.hot.on('piny:astro-hmr', async () => { try { if (typeof pinyPhone === 'undefined' || !pinyPhone.isEnabled()) { console.debug(`[piny-astro] Visual select is disabled. Full reload.`); location.reload(); return; } const res = await fetch(location.href, { headers: { accept: 'text/html', 'x-piny-hmr': '1' }, cache: 'no-cache' }); const html = await res.text(); parser = parser || new DOMParser(); const nextDoc = parser.parseFromString(html, 'text/html'); /* diff the two documents and decide whether it’s safe to morph */ const diff = micromorph.diff(document, nextDoc); const guard = isDiffSafe(document.documentElement, diff); if (typeof guard === 'string') { console.debug(`[piny-astro] full reload (${guard})`); location.reload(); } else { console.debug('[piny-astro] morphing'); await micromorph.patch(document, diff); } } catch (err) { console.error('[piny-astro] soft HMR failed – fallback', err); location.reload(); } }); } /* ───────── helper heuristics (adapted from astro-pages-hmr) ───────── */ function isDiffSafe(parent, diff, child) { const el = child || parent; switch (diff.type) { /* ACTION_CREATE = 0 */ case 0: { return; const tag = diff.node?.tagName; return isTagNameUnsafe(tag) ? `${tag.toLowerCase()} created` : undefined; } /* ACTION_REMOVE = 1 */ case 1: { return; if (!el) return; const tag = el.tagName; return isTagNameUnsafe(tag) ? `${tag.toLowerCase()} removed` : undefined; } /* ACTION_REPLACE = 2 */ case 2: { return; if (!el) return; const tagA = el.tagName; const tagB = diff.node?.tagName; if (isTagNameUnsafe(tagA)) return `${tagA.toLowerCase()} replaced`; if (isTagNameUnsafe(tagB)) return `${tagB.toLowerCase()} replaced`; return; } /* ACTION_UPDATE = 3 */ case 3: { if (!el) return; const tag = el.tagName; if (diff.attributes.length && isTagNameUnsafe(tag)) { return `${tag.toLowerCase()} attribute updated`; } sanitizeDiffChildren(diff.children, el); for (let i = 0; i < diff.children.length; i++) { const childDiff = diff.children[i]; if (!childDiff) continue; const childNode = el.childNodes[i]; const violation = isDiffSafe(el, childDiff, childNode); if (violation) return violation; } return; } } } function isTagNameUnsafe(tag) { return ( tag && (tag === 'SCRIPT' || (tag.includes('-') && tag !== 'ASTRO-DEV-TOOLBAR')) ); } const dynamicIslandAttrs = ['client-render-time', 'server-render-time', 'ssr']; function sanitizeDiffChildren(patches, el) { for (let i = 0; i < patches.length; i++) { const patch = patches[i]; if (!patch || patch.type !== 3) continue; // we only care about updates const tag = el.childNodes[i]?.tagName; if (tag === 'ASTRO-ISLAND' || tag === 'SCRIPT') { patches[i] = undefined; continue; } if (tag === 'ASTRO-ISLAND' && patch.attributes.length) { patch.attributes = patch.attributes.filter( (a) => !dynamicIslandAttrs.includes(a.name) ); if (patch.attributes.length === 0) patches[i] = undefined; continue; } if (tag === 'SCRIPT' && patch.attributes.length) { const curDoc = document; patch.attributes = patch.attributes.filter((attr) => { /* drop view-transition runtime markers */ if (attr.type === 5 && attr.name === 'data-astro-exec') return false; /* ignore src mutations that only update timestamp cache-bust */ if (attr.type === 4 && attr.name === 'src') { const cleanSrc = attr.value .replace(/(\?|&)t=.*?(&|$)/, (_, m1, m2) => (m2 ? m1 : '')) .replace(/\?$/, ''); if (curDoc.querySelector(`script[src="${cleanSrc}"]`)) return false; } return true; }); if (patch.attributes.length === 0) patches[i] = undefined; } } }