@pinegrow/piny-astro
Version:
An Astro plugin that implements Piny integration in dev mode.
130 lines (113 loc) • 4.78 kB
JavaScript
/* ────────────────────────────────────────────────────────────────────────────
* PinyAstro – dev-tools integration
*
* • keeps the original `piny.phone.js` injection
* • duplicates `data-astro-source-*` ➜ `data-pg-source-*`
* • adds “soft-HMR” for every `.astro` file via a runtime loaded through Vite
*
* ✱ dev-dependency --> npm i -D micromorph
* ──────────────────────────────────────────────────────────────────────────── */
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
const requireCwd = createRequire(process.cwd() + '/noop.js');
export default function PinyAstro(options = {}) {
const { injectScript = true, hotReload = true } = options;
/* on-disk helper scripts -------------------------------------------------- */
const dir = path.dirname(fileURLToPath(import.meta.url));
const phoneId = '\0virtual:piny-phone';
const phonePath = path.resolve(dir, 'piny.phone.js');
const runtimeId = '\0virtual:piny-astro-runtime';
const runtimePath = path.resolve(dir, 'piny-astro-runtime.js');
return {
name: 'piny-astro-dev-tools',
hooks: {
/* ─────────────────────── config stage ─────────────────────────────── */
'astro:config:setup'({ command, injectScript: astroInject, updateConfig }) {
if (command !== 'dev') return; // nothing to do for build / preview
/* 1️⃣ inject both helper scripts into every page */
if (injectScript) {
astroInject('page', `import "${phoneId}";`);
}
if(hotReload) {
try {
// will throw if the module cannot be resolved from cwd
requireCwd.resolve('micromorph');
astroInject('page', `import "${runtimeId}";`);
} catch {
throw new Error(
'[piny-astro] "micromorph" is required for soft-HMR.\n' +
'Please install it with: npm i -D micromorph'
)
}
}
const plugins = [
debugLocatorPlugin(),
virtualFilePlugin({ id: phoneId, path: phonePath }),
]
if(hotReload) {
plugins.push(virtualFilePlugin({ id: runtimeId, path: runtimePath }))
plugins.push(softAstroHmrPlugin())
}
/* 2️⃣ merge our Vite plugins into the user config */
updateConfig({
vite: {
plugins: plugins
},
});
},
},
};
}
/* ───────── Vite plugin: duplicate data-attrs for devtools ───────── */
function debugLocatorPlugin() {
const hasPgFile = /\bdata-pg-source-file=/;
const hasPgLoc = /\bdata-pg-source-loc=/;
return {
name: 'vite-plugin-piny-data-astro-dup',
apply: 'serve',
enforce: 'post',
transform(code, id) {
const isAstroFile = id.endsWith('.astro');
if (!isAstroFile) return;
if (hasPgFile.test(code) && hasPgLoc.test(code)) return; // idempotent
const out = code
.replace(
/data-astro-source-file="([^"]*)"(?![^>]*\bdata-pg-source-file\b)/g,
'data-astro-source-file="$1" data-pg-source-file="$1"',
)
.replace(
/data-astro-source-loc="([^"]*)"(?![^>]*\bdata-pg-source-loc\b)/g,
'data-astro-source-loc="$1" data-pg-source-loc="$1"',
);
return out === code ? undefined : { code: out, map: null };
},
};
}
/* ───── Vite plugin: serve any on-disk file as a virtual module ───── */
function virtualFilePlugin({ id, path: filePath }) {
return {
name: `vite-plugin-${id.replace(/\0|:/g, '')}`,
apply: 'serve',
resolveId(source) {
return source === id ? id : undefined;
},
load(source) {
return source === id ? fs.readFileSync(filePath, 'utf-8') : undefined;
},
};
}
/* ───── Vite plugin: broadcast “piny:astro-hmr” on every .astro save ───── */
function softAstroHmrPlugin() {
return {
name: 'vite-plugin-piny-astro-soft-hmr',
apply: 'serve',
enforce: 'post',
handleHotUpdate({ file, server }) {
if (!file.endsWith('.astro')) return;
server.ws.send({ type: 'custom', event: 'piny:astro-hmr', data: { file } });
return []; // stop Vite’s default hard reload
},
};
}