Packages

React inside Phoenix LiveView — async code splitting, client-side props diffing, and zero-config component discovery.

Retired package: Release invalid - Not production-ready, LiveView 1.0.x compatibility issue

Current section

Files

Jump to
react_phx assets dist vite-plugin.js
Raw

assets/dist/vite-plugin.js

import { existsSync } from "node:fs";
import { resolve } from "node:path";
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
function hotUpdateType(path) {
if (path.endsWith("css"))
return "css-update";
if (path.endsWith("js"))
return "js-update";
return null;
}
function jsonResponse(res, statusCode, data) {
res.statusCode = statusCode;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(data));
}
/** Simple JSON body parser middleware. */
function jsonMiddleware(req, res, next) {
let data = "";
req.on("data", (chunk) => {
data += chunk;
});
req.on("end", () => {
try {
req.body = JSON.parse(data);
next();
}
catch {
jsonResponse(res, 400, { error: "Invalid JSON" });
}
});
req.on("error", (err) => {
console.error(err);
jsonResponse(res, 500, { error: "Internal Server Error" });
});
}
// ---------------------------------------------------------------------------
// Plugin
// ---------------------------------------------------------------------------
export default function reactPhxPlugin(opts = {}) {
const componentDir = opts.componentDir ?? "./react-components";
const hmr = opts.hmr ?? true;
const vendorChunks = opts.vendorChunks ?? true;
const ssr = opts.ssr ?? true;
const ssrPath = opts.ssrPath ?? opts.path ?? "/ssr_render";
const ssrEntry = opts.ssrEntry ?? opts.entrypoint ?? "./js/server.js";
let viteRoot = process.cwd();
return {
name: "react-phx",
// --- Capture resolved Vite root for glob path resolution ---
configResolved(config) {
viteRoot = config.root;
},
// --- Virtual module resolution ---
resolveId(id) {
if (id === "virtual:react-phx")
return "\0virtual:react-phx";
if (id === "virtual:react-phx/server")
return "\0virtual:react-phx/server";
},
// --- Virtual module content ---
load(id) {
// In virtual modules, Vite requires glob patterns to start with '/'.
// A leading '/' means "relative to the Vite project root", so we
// compute the path relative to root and prepend '/'.
const absDir = resolve(viteRoot, componentDir);
const relFromRoot = absDir === viteRoot
? ""
: "/" + absDir.slice(viteRoot.length + 1).split("\\").join("/");
if (id === "\0virtual:react-phx") {
return `
import { getHooks } from "react_phx";
const components = import.meta.glob("${relFromRoot}/**/*.{jsx,tsx}", { eager: false });
const hooks = getHooks(components);
export default hooks;
export { components };
`;
}
if (id === "\0virtual:react-phx/server") {
return `
import { getRender } from "react_phx/server";
const components = import.meta.glob("${relFromRoot}/**/*.{jsx,tsx}", { eager: true });
export const render = getRender(components);
`;
}
},
// --- Automatic vendor chunk splitting ---
config(userConfig, { command }) {
if (command !== "build" || !vendorChunks)
return;
// Don't override user's manualChunks
const output = userConfig.build?.rollupOptions?.output;
if (output && (Array.isArray(output) || output.manualChunks))
return;
return {
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes("node_modules/react/") ||
id.includes("node_modules/react-dom/"))
return "vendor-react";
if (id.includes("/deps/phoenix"))
return "vendor-phoenix";
},
},
},
},
};
},
// --- HMR for .heex/.ex files ---
handleHotUpdate({ file, modules, server, timestamp }) {
if (!hmr)
return;
if (file.match(/\.(heex|ex)$/)) {
const invalidatedModules = new Set();
for (const mod of modules) {
server.moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true);
}
const updates = Array.from(invalidatedModules)
.filter((m) => m.file && hotUpdateType(m.file))
.map((m) => ({
type: hotUpdateType(m.file),
path: m.url,
acceptedPath: m.url,
timestamp,
}));
server.ws.send({ type: "update", updates });
// We handle the hot update ourselves
return [];
}
},
// --- SSR middleware ---
configureServer(server) {
// Terminate the watcher when Phoenix quits
process.stdin.on("close", () => process.exit(0));
process.stdin.resume();
// Determine the SSR entrypoint:
// 1. If user set explicit ssrEntry/entrypoint, use it.
// 2. Else if ssr is enabled and the default server.js doesn't exist, use the virtual module.
let resolvedEntry = ssrEntry;
if (!opts.ssrEntry &&
!opts.entrypoint &&
ssr &&
!existsSync(resolve(process.cwd(), ssrEntry))) {
resolvedEntry = "virtual:react-phx/server";
}
server.middlewares.use(function reactPhxMiddleware(req, res, next) {
if (req.method === "POST" &&
req.url?.split("?", 1)[0] === ssrPath) {
jsonMiddleware(req, res, async () => {
try {
const mod = (await server.ssrLoadModule(resolvedEntry));
const html = await mod.render(req.body.name, req.body.props, req.body.slots);
res.end(html);
}
catch (e) {
server.ssrFixStacktrace(e);
jsonResponse(res, 500, { error: e });
}
});
}
else {
next();
}
});
},
};
}