@pinegrow/piny-astro
Version:
An Astro plugin that implements Piny integration in dev mode.
145 lines (124 loc) • 5.12 kB
JavaScript
/// <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;
}
}
}