import type { Plugin } from "vite";
import { resolve } from "node:path";
import * as Vite from "vite";
import { resolveRscEntryFromConfig } from "../utils/shared-utils.js";
/**
* Plugin that auto-injects VERSION and routes-manifest into custom entry.rsc files.
* If a custom entry.rsc file uses createRSCHandler but doesn't pass version,
* this transform adds the import and property automatically.
* Also ensures the routes-manifest virtual module is always imported.
* @internal
*/
export function createVersionInjectorPlugin(
rscEntryPath: string | undefined,
): Plugin {
let resolvedEntryPath = "";
return {
name: "@rangojs/router:version-injector",
enforce: "pre",
configResolved(config) {
let entryPath = rscEntryPath;
if (!entryPath) entryPath = resolveRscEntryFromConfig(config);
if (entryPath) {
resolvedEntryPath = resolve(config.root, entryPath);
}
},
transform(code, id) {
if (!resolvedEntryPath) return null;
// Only transform the RSC entry file
const normalizedId = Vite.normalizePath(id);
const normalizedEntry = Vite.normalizePath(resolvedEntryPath);
if (normalizedId !== normalizedEntry) {
return null;
}
// Always prepend `import "virtual:rsc-router/routes-manifest"` as the
// first side-effect import. The manifest virtual module's `load()` hook
// awaits `s.discoveryDone` so that, by the time the rest of the entry
// including any module-level `router.reverse()` calls under `./router.js`
// evaluates, runtime discovery has rewritten `router.named-routes.gen.ts`
// with the full route table.
//
// ES module evaluation order matters here: while imports are *parsed*
// hoisted, side-effect imports are evaluated in source order in the
// dependency graph. A user-authored `import "virtual:rsc-router/..."`
// placed after `import "./router.js"` runs too late: the manifest
// gate fires after router.tsx has already crashed on a stale gen file.
// We always prepend; ESM dedups any user-written duplicate, so module
// initialization still runs once.
const prepend: string[] = [
`import "virtual:rsc-router/routes-manifest";`,
];
// Auto-inject VERSION if file uses createRSCHandler without version
let newCode = code;
const needsVersion =
code.includes("createRSCHandler") &&
!code.includes("@rangojs/router:version") &&
/createRSCHandler\s*\(\s*\{/.test(code);
if (needsVersion) {
prepend.push(`import { VERSION } from "@rangojs/router:version";`);
newCode = newCode.replace(
/createRSCHandler\s*\(\s*\{/,
"createRSCHandler({\n version: VERSION,",
);
}
// Insert after any leading `/// ` triple-slash
// directives (and surrounding blank lines). TypeScript requires those
// directives to precede all other code; putting our imports above
// them silently demotes the directives to plain comments.
const lines = newCode.split("\n");
let insertAt = 0;
while (insertAt < lines.length) {
const trimmed = lines[insertAt]!.trim();
if (trimmed === "" || /^\/\/\/\s*