vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
126 lines (117 loc) • 11.4 kB
JavaScript
/**
* vite-plugin-react-server
* Copyright (c) Nico Brinkkemper
* MIT License
*/
const VIRTUAL_RSC_HMR = "virtual:react-server/hmr";
const RESOLVED_VIRTUAL_RSC_HMR = "\0" + VIRTUAL_RSC_HMR;
const VIRTUAL_RSC_HMR_SOURCE = (
/* js */
`
import { useEffect, useCallback } from "react";
const RSC_HMR_EVENT = 'vite-plugin-react-server:server-component-update';
export { RSC_HMR_EVENT };
// Cache-bust every <link rel="stylesheet"> whose href references the changed
// file. Server-collected CSS (vprs's <Css cssFiles={...}/> pattern) isn't in
// the client module graph, so Vite's native CSS HMR never fires for these —
// the <link> URL doesn't change and the browser keeps the cached stylesheet.
// data.file is the project-relative path (eg "src/css/9mmc.module.css") and
// the link href is a URL whose pathname ends with the same suffix.
// We also fall back to basename matching to handle edge cases where the
// project-relative form doesn't appear verbatim in the href.
function refreshCssLinks(data) {
if (!data) return false;
const rel = String(data.file || '').replace(/^[\\\\/]+/, '');
if (!rel) return false;
const basename = rel.split(/[\\\\/]/).pop();
const links = document.querySelectorAll('link[rel="stylesheet"]');
let refreshed = 0;
for (const link of links) {
const href = link.getAttribute('href');
if (!href) continue;
let pathname;
try {
pathname = new URL(href, window.location.origin).pathname;
} catch {
pathname = href.split('?')[0];
}
const matches =
pathname.endsWith('/' + rel) ||
pathname.endsWith(rel) ||
(basename && pathname.endsWith('/' + basename));
if (!matches) continue;
const url = new URL(href, window.location.origin);
url.searchParams.set('t', String(Date.now()));
link.setAttribute('href', url.pathname + url.search);
refreshed++;
}
return refreshed > 0;
}
export function useRscHmr(refetch, options = {}) {
const { verbose = true, filter } = options;
const handler = useCallback(
(data) => {
if (filter && !filter(data)) return;
if (verbose) {
console.log('[RSC HMR] Server component updated:', data.file, data.kind ? '(' + data.kind + ')' : '');
}
if (data && data.kind === 'css') {
refreshCssLinks(data);
}
refetch(window.location.pathname);
},
[refetch, verbose, filter]
);
useEffect(() => {
if (typeof import.meta.hot === 'undefined') return;
import.meta.hot.on(RSC_HMR_EVENT, handler);
if (verbose) {
console.log('[RSC HMR] Listening for server component updates');
}
return () => {
import.meta.hot.off(RSC_HMR_EVENT, handler);
};
}, [handler, verbose]);
}
export function setupRscHmr(options = {}) {
const { onUpdate, verbose = true } = options;
if (typeof import.meta.hot === 'undefined') return;
import.meta.hot.on(RSC_HMR_EVENT, async (data) => {
if (verbose) {
console.log('[RSC HMR] Server component updated:', data.file, data.kind ? '(' + data.kind + ')' : '');
}
if (data && data.kind === 'css') {
refreshCssLinks(data);
}
if (onUpdate === 'reload') {
window.location.reload();
return;
}
if (onUpdate) {
try { await onUpdate(data); }
catch (error) { console.error('[RSC HMR] Error in onUpdate handler:', error); window.location.reload(); }
} else if (!data || data.kind !== 'css') {
// For non-CSS updates without a handler, fall back to reload.
// For CSS updates, refreshCssLinks already handled it visually.
window.location.reload();
}
});
if (verbose) {
console.log('[RSC HMR] Listening for server component updates');
}
}
`
);
function virtualRscHmrPlugin() {
return {
name: "vite-plugin-react-server:virtual-rsc-hmr",
resolveId(id) {
if (id === VIRTUAL_RSC_HMR) return RESOLVED_VIRTUAL_RSC_HMR;
},
load(id) {
if (id === RESOLVED_VIRTUAL_RSC_HMR) return VIRTUAL_RSC_HMR_SOURCE;
}
};
}
export { virtualRscHmrPlugin };
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmlydHVhbFJzY0htclBsdWdpbi5qcyIsInNvdXJjZXMiOlsiLi4vLi4vLi4vcGx1Z2luL2Rldi1zZXJ2ZXIvdmlydHVhbFJzY0htclBsdWdpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgdHlwZSB7IFBsdWdpbiB9IGZyb20gXCJ2aXRlXCI7XG5cbmNvbnN0IFZJUlRVQUxfUlNDX0hNUiA9ICd2aXJ0dWFsOnJlYWN0LXNlcnZlci9obXInO1xuY29uc3QgUkVTT0xWRURfVklSVFVBTF9SU0NfSE1SID0gJ1xcMCcgKyBWSVJUVUFMX1JTQ19ITVI7XG5cbi8qKlxuICogVmlydHVhbCBtb2R1bGUgc291cmNlIGZvciBgdmlydHVhbDpyZWFjdC1zZXJ2ZXIvaG1yYC5cbiAqIFNlbGYtY29udGFpbmVkIOKAlCBubyBpbXBvcnRzIGZyb20gdGhlIHBsdWdpbiBwYWNrYWdlLCBzbyBubyBkZXAgcmUtb3B0aW1pemF0aW9uLlxuICogSE1SIGNvZGUgaXMgZGVhZC1jb2RlIGVsaW1pbmF0ZWQgaW4gcHJvZHVjdGlvbiBidWlsZHMgKGltcG9ydC5tZXRhLmhvdCBpcyB1bmRlZmluZWQpLlxuICovXG5jb25zdCBWSVJUVUFMX1JTQ19ITVJfU09VUkNFID0gLyoganMgKi9gXG5pbXBvcnQgeyB1c2VFZmZlY3QsIHVzZUNhbGxiYWNrIH0gZnJvbSBcInJlYWN0XCI7XG5cbmNvbnN0IFJTQ19ITVJfRVZFTlQgPSAndml0ZS1wbHVnaW4tcmVhY3Qtc2VydmVyOnNlcnZlci1jb21wb25lbnQtdXBkYXRlJztcblxuZXhwb3J0IHsgUlNDX0hNUl9FVkVOVCB9O1xuXG4vLyBDYWNoZS1idXN0IGV2ZXJ5IDxsaW5rIHJlbD1cInN0eWxlc2hlZXRcIj4gd2hvc2UgaHJlZiByZWZlcmVuY2VzIHRoZSBjaGFuZ2VkXG4vLyBmaWxlLiBTZXJ2ZXItY29sbGVjdGVkIENTUyAodnBycydzIDxDc3MgY3NzRmlsZXM9ey4uLn0vPiBwYXR0ZXJuKSBpc24ndCBpblxuLy8gdGhlIGNsaWVudCBtb2R1bGUgZ3JhcGgsIHNvIFZpdGUncyBuYXRpdmUgQ1NTIEhNUiBuZXZlciBmaXJlcyBmb3IgdGhlc2Ug4oCUXG4vLyB0aGUgPGxpbms+IFVSTCBkb2Vzbid0IGNoYW5nZSBhbmQgdGhlIGJyb3dzZXIga2VlcHMgdGhlIGNhY2hlZCBzdHlsZXNoZWV0LlxuLy8gZGF0YS5maWxlIGlzIHRoZSBwcm9qZWN0LXJlbGF0aXZlIHBhdGggKGVnIFwic3JjL2Nzcy85bW1jLm1vZHVsZS5jc3NcIikgYW5kXG4vLyB0aGUgbGluayBocmVmIGlzIGEgVVJMIHdob3NlIHBhdGhuYW1lIGVuZHMgd2l0aCB0aGUgc2FtZSBzdWZmaXguXG4vLyBXZSBhbHNvIGZhbGwgYmFjayB0byBiYXNlbmFtZSBtYXRjaGluZyB0byBoYW5kbGUgZWRnZSBjYXNlcyB3aGVyZSB0aGVcbi8vIHByb2plY3QtcmVsYXRpdmUgZm9ybSBkb2Vzbid0IGFwcGVhciB2ZXJiYXRpbSBpbiB0aGUgaHJlZi5cbmZ1bmN0aW9uIHJlZnJlc2hDc3NMaW5rcyhkYXRhKSB7XG4gIGlmICghZGF0YSkgcmV0dXJuIGZhbHNlO1xuICBjb25zdCByZWwgPSBTdHJpbmcoZGF0YS5maWxlIHx8ICcnKS5yZXBsYWNlKC9eW1xcXFxcXFxcL10rLywgJycpO1xuICBpZiAoIXJlbCkgcmV0dXJuIGZhbHNlO1xuICBjb25zdCBiYXNlbmFtZSA9IHJlbC5zcGxpdCgvW1xcXFxcXFxcL10vKS5wb3AoKTtcbiAgY29uc3QgbGlua3MgPSBkb2N1bWVudC5xdWVyeVNlbGVjdG9yQWxsKCdsaW5rW3JlbD1cInN0eWxlc2hlZXRcIl0nKTtcbiAgbGV0IHJlZnJlc2hlZCA9IDA7XG4gIGZvciAoY29uc3QgbGluayBvZiBsaW5rcykge1xuICAgIGNvbnN0IGhyZWYgPSBsaW5rLmdldEF0dHJpYnV0ZSgnaHJlZicpO1xuICAgIGlmICghaHJlZikgY29udGludWU7XG4gICAgbGV0IHBhdGhuYW1lO1xuICAgIHRyeSB7XG4gICAgICBwYXRobmFtZSA9IG5ldyBVUkwoaHJlZiwgd2luZG93LmxvY2F0aW9uLm9yaWdpbikucGF0aG5hbWU7XG4gICAgfSBjYXRjaCB7XG4gICAgICBwYXRobmFtZSA9IGhyZWYuc3BsaXQoJz8nKVswXTtcbiAgICB9XG4gICAgY29uc3QgbWF0Y2hlcyA9XG4gICAgICBwYXRobmFtZS5lbmRzV2l0aCgnLycgKyByZWwpIHx8XG4gICAgICBwYXRobmFtZS5lbmRzV2l0aChyZWwpIHx8XG4gICAgICAoYmFzZW5hbWUgJiYgcGF0aG5hbWUuZW5kc1dpdGgoJy8nICsgYmFzZW5hbWUpKTtcbiAgICBpZiAoIW1hdGNoZXMpIGNvbnRpbnVlO1xuICAgIGNvbnN0IHVybCA9IG5ldyBVUkwoaHJlZiwgd2luZG93LmxvY2F0aW9uLm9yaWdpbik7XG4gICAgdXJsLnNlYXJjaFBhcmFtcy5zZXQoJ3QnLCBTdHJpbmcoRGF0ZS5ub3coKSkpO1xuICAgIGxpbmsuc2V0QXR0cmlidXRlKCdocmVmJywgdXJsLnBhdGhuYW1lICsgdXJsLnNlYXJjaCk7XG4gICAgcmVmcmVzaGVkKys7XG4gIH1cbiAgcmV0dXJuIHJlZnJlc2hlZCA+IDA7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiB1c2VSc2NIbXIocmVmZXRjaCwgb3B0aW9ucyA9IHt9KSB7XG4gIGNvbnN0IHsgdmVyYm9zZSA9IHRydWUsIGZpbHRlciB9ID0gb3B0aW9ucztcblxuICBjb25zdCBoYW5kbGVyID0gdXNlQ2FsbGJhY2soXG4gICAgKGRhdGEpID0+IHtcbiAgICAgIGlmIChmaWx0ZXIgJiYgIWZpbHRlcihkYXRhKSkgcmV0dXJuO1xuICAgICAgaWYgKHZlcmJvc2UpIHtcbiAgICAgICAgY29uc29sZS5sb2coJ1tSU0MgSE1SXSBTZXJ2ZXIgY29tcG9uZW50IHVwZGF0ZWQ6JywgZGF0YS5maWxlLCBkYXRhLmtpbmQgPyAnKCcgKyBkYXRhLmtpbmQgKyAnKScgOiAnJyk7XG4gICAgICB9XG4gICAgICBpZiAoZGF0YSAmJiBkYXRhLmtpbmQgPT09ICdjc3MnKSB7XG4gICAgICAgIHJlZnJlc2hDc3NMaW5rcyhkYXRhKTtcbiAgICAgIH1cbiAgICAgIHJlZmV0Y2god2luZG93LmxvY2F0aW9uLnBhdGhuYW1lKTtcbiAgICB9LFxuICAgIFtyZWZldGNoLCB2ZXJib3NlLCBmaWx0ZXJdXG4gICk7XG5cbiAgdXNlRWZmZWN0KCgpID0+IHtcbiAgICBpZiAodHlwZW9mIGltcG9ydC5tZXRhLmhvdCA9PT0gJ3VuZGVmaW5lZCcpIHJldHVybjtcbiAgICBpbXBvcnQubWV0YS5ob3Qub24oUlNDX0hNUl9FVkVOVCwgaGFuZGxlcik7XG4gICAgaWYgKHZlcmJvc2UpIHtcbiAgICAgIGNvbnNvbGUubG9nKCdbUlNDIEhNUl0gTGlzdGVuaW5nIGZvciBzZXJ2ZXIgY29tcG9uZW50IHVwZGF0ZXMnKTtcbiAgICB9XG4gICAgcmV0dXJuICgpID0+IHtcbiAgICAgIGltcG9ydC5tZXRhLmhvdC5vZmYoUlNDX0hNUl9FVkVOVCwgaGFuZGxlcik7XG4gICAgfTtcbiAgfSwgW2hhbmRsZXIsIHZlcmJvc2VdKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHNldHVwUnNjSG1yKG9wdGlvbnMgPSB7fSkge1xuICBjb25zdCB7IG9uVXBkYXRlLCB2ZXJib3NlID0gdHJ1ZSB9ID0gb3B0aW9ucztcbiAgaWYgKHR5cGVvZiBpbXBvcnQubWV0YS5ob3QgPT09ICd1bmRlZmluZWQnKSByZXR1cm47XG4gIGltcG9ydC5tZXRhLmhvdC5vbihSU0NfSE1SX0VWRU5ULCBhc3luYyAoZGF0YSkgPT4ge1xuICAgIGlmICh2ZXJib3NlKSB7XG4gICAgICBjb25zb2xlLmxvZygnW1JTQyBITVJdIFNlcnZlciBjb21wb25lbnQgdXBkYXRlZDonLCBkYXRhLmZpbGUsIGRhdGEua2luZCA/ICcoJyArIGRhdGEua2luZCArICcpJyA6ICcnKTtcbiAgICB9XG4gICAgaWYgKGRhdGEgJiYgZGF0YS5raW5kID09PSAnY3NzJykge1xuICAgICAgcmVmcmVzaENzc0xpbmtzKGRhdGEpO1xuICAgIH1cbiAgICBpZiAob25VcGRhdGUgPT09ICdyZWxvYWQnKSB7XG4gICAgICB3aW5kb3cubG9jYXRpb24ucmVsb2FkKCk7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChvblVwZGF0ZSkge1xuICAgICAgdHJ5IHsgYXdhaXQgb25VcGRhdGUoZGF0YSk7IH1cbiAgICAgIGNhdGNoIChlcnJvcikgeyBjb25zb2xlLmVycm9yKCdbUlNDIEhNUl0gRXJyb3IgaW4gb25VcGRhdGUgaGFuZGxlcjonLCBlcnJvcik7IHdpbmRvdy5sb2NhdGlvbi5yZWxvYWQoKTsgfVxuICAgIH0gZWxzZSBpZiAoIWRhdGEgfHwgZGF0YS5raW5kICE9PSAnY3NzJykge1xuICAgICAgLy8gRm9yIG5vbi1DU1MgdXBkYXRlcyB3aXRob3V0IGEgaGFuZGxlciwgZmFsbCBiYWNrIHRvIHJlbG9hZC5cbiAgICAgIC8vIEZvciBDU1MgdXBkYXRlcywgcmVmcmVzaENzc0xpbmtzIGFscmVhZHkgaGFuZGxlZCBpdCB2aXN1YWxseS5cbiAgICAgIHdpbmRvdy5sb2NhdGlvbi5yZWxvYWQoKTtcbiAgICB9XG4gIH0pO1xuICBpZiAodmVyYm9zZSkge1xuICAgIGNvbnNvbGUubG9nKCdbUlNDIEhNUl0gTGlzdGVuaW5nIGZvciBzZXJ2ZXIgY29tcG9uZW50IHVwZGF0ZXMnKTtcbiAgfVxufVxuYDtcblxuLyoqXG4gKiBQbHVnaW4gdGhhdCBwcm92aWRlcyB0aGUgYHZpcnR1YWw6cmVhY3Qtc2VydmVyL2htcmAgbW9kdWxlLlxuICogV29ya3MgaW4gYm90aCBkZXYgYW5kIGJ1aWxkIOKAlCBITVIgY29kZSB0cmVlLXNoYWtlcyBhd2F5IGluIHByb2R1Y3Rpb24uXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiB2aXJ0dWFsUnNjSG1yUGx1Z2luKCk6IFBsdWdpbiB7XG4gIHJldHVybiB7XG4gICAgbmFtZTogXCJ2aXRlLXBsdWdpbi1yZWFjdC1zZXJ2ZXI6dmlydHVhbC1yc2MtaG1yXCIsXG4gICAgcmVzb2x2ZUlkKGlkKSB7XG4gICAgICBpZiAoaWQgPT09IFZJUlRVQUxfUlNDX0hNUikgcmV0dXJuIFJFU09MVkVEX1ZJUlRVQUxfUlNDX0hNUjtcbiAgICB9LFxuICAgIGxvYWQoaWQpIHtcbiAgICAgIGlmIChpZCA9PT0gUkVTT0xWRURfVklSVFVBTF9SU0NfSE1SKSByZXR1cm4gVklSVFVBTF9SU0NfSE1SX1NPVVJDRTtcbiAgICB9LFxuICB9O1xufVxuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBRUEsTUFBTSxlQUFrQixHQUFBLDBCQUFBO0FBQ3hCLE1BQU0sMkJBQTJCLElBQU8sR0FBQSxlQUFBO0FBT3hDLE1BQU0sc0JBQUE7QUFBQTtBQUFBLEVBQWlDO0FBQUE7O0FBQUE7O0FBQUE7O0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBOztBQUFBO0FBQUE7O0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7O0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBLENBQUE7QUEwR2hDLFNBQVMsbUJBQThCLEdBQUE7QUFDckMsRUFBQSxPQUFBO0FBQUEsSUFDTCxJQUFNLEVBQUEsMENBQUE7QUFBQSxJQUNOLFVBQVUsRUFBSSxFQUFBO0FBQ1IsTUFBQSxJQUFBLEVBQUEsS0FBTyxpQkFBd0IsT0FBQSx3QkFBQTtBQUFBLEtBQ3JDO0FBQUEsSUFDQSxLQUFLLEVBQUksRUFBQTtBQUNILE1BQUEsSUFBQSxFQUFBLEtBQU8sMEJBQWlDLE9BQUEsc0JBQUE7QUFBQTtBQUM5QyxHQUNGO0FBQ0Y7Ozs7In0=