rakkasjs
Version:
Bleeding-edge React framework powered by Vite
1,514 lines (1,476 loc) • 51.5 kB
JavaScript
// src/vite-plugin/index.ts
import react from "@vitejs/plugin-react";
// src/vite-plugin/inject-config.ts
import { spawn } from "child_process";
import glob from "fast-glob";
import pico from "picocolors";
import micromatch from "micromatch";
import path from "path";
import { pathToFileURL } from "url";
function injectConfig(options) {
return {
name: "rakkasjs:inject-config",
enforce: "pre",
async config(_, env) {
if (!process.env.RAKKAS_BUILD_ID) {
process.env.RAKKAS_BUILD_ID = env.command === "serve" ? "development" : await getBuildId();
}
if (options.adapter.disableStreaming) {
process.env.RAKKAS_DISABLE_STREAMING = "true";
} else {
process.env.RAKKAS_DISABLE_STREAMING = "false";
}
const buildSteps = [
{
name: "client",
config: {
build: {
outDir: "dist/client",
rollupOptions: {
input: {
index: "/virtual:rakkasjs:client-entry"
}
}
}
}
},
{
name: "server",
config: {
build: {
outDir: "dist/server",
ssr: true,
rollupOptions: {
input: {
index: "/virtual:vavite-connect-server",
hattip: "virtual:rakkasjs:hattip-entry"
}
}
}
}
}
];
return {
buildSteps,
ssr: {
external: ["react-dom/server.browser"],
noExternal: ["rakkasjs", "@vavite/expose-vite-dev-server"],
optimizeDeps: {
exclude: [
"rakkasjs",
"@vavite/expose-vite-dev-server",
"virtual:rakkasjs:client-manifest",
"virtual:rakkasjs:client-page-routes",
"virtual:rakkasjs:api-routes",
"virtual:rakkasjs:run-server-side:manifest",
"virtual:rakkasjs:server-page-routes",
"virtual:rakkasjs:error-page"
]
}
},
appType: "custom",
optimizeDeps: {
include: ["react", "react-dom", "react-dom/client"],
// TODO: Remove this when https://github.com/vitejs/vite/pull/8917 is merged
exclude: [
"rakkasjs",
"virtual:rakkasjs:client-manifest",
"virtual:rakkasjs:client-page-routes",
"virtual:rakkasjs:api-routes",
"virtual:rakkasjs:run-server-side:manifest",
"virtual:rakkasjs:server-page-routes",
"virtual:rakkasjs:error-page",
"@vavite/expose-vite-dev-server"
]
},
envPrefix: "RAKKAS_",
api: {
rakkas: {
prerender: options.prerender,
adapter: options.adapter
}
},
define: {
"process.env.RAKKAS_STRICT_MODE": JSON.stringify(
options.strictMode.toString()
)
}
};
},
async configResolved(config) {
const routeConfigFiles = await glob(
config.root + "/src/routes/**/route.config.js"
);
const routeConfigContents = await Promise.allSettled(
routeConfigFiles.map((file) => {
const specifier = pathToFileURL(file) + `?${cacheBuster}`;
return import(specifier).then((module) => module.default);
})
);
cacheBuster++;
const routeConfigs = routeConfigContents.map((result, i) => ({
file: routeConfigFiles[i].slice(
config.root.length + "/src/routes/".length
),
...result
}));
const failed = routeConfigs.find(
(c) => c.status === "rejected"
);
if (failed) {
const message = `Failed to load ${failed.file}: ${failed.reason.stack}`;
if (config.command === "build") {
throw new Error(message);
} else {
config.logger.error(pico.red(message));
}
}
config.configFileDependencies.push(...routeConfigFiles);
const writable = config;
writable.api = writable.api || {};
writable.api.rakkas = writable.api.rakkas || {};
writable.api.rakkas.routeConfigs = [];
for (const routeConfig of routeConfigs) {
if (routeConfig.status === "fulfilled") {
try {
const value = typeof routeConfig.value === "function" ? routeConfig.value(config) : routeConfig.value;
writable.api.rakkas.routeConfigs.push({
dir: routeConfig.file.slice(0, -"route.config.js".length).replace(/\\/g, "/"),
value
});
} catch (error) {
const message = `Failed to evaluate ${routeConfig.file}: ${error?.stack}`;
if (config.command === "build") {
throw new Error(message);
} else {
config.logger.error(pico.red(message));
}
}
}
}
writable.api.rakkas.routeConfigs.sort(
(a, b) => a.dir.length - b.dir.length
);
},
configureServer(server) {
const isRouteConfigFile = micromatch.matcher(
server.config.root + "/src/routes/**/route.config.js"
);
server.watcher.addListener("all", (ev, file) => {
if (isRouteConfigFile(file) && (ev === "add" || ev === "unlink")) {
server.config.logger.info(
pico.green(
`${path.relative(
process.cwd(),
file
)} changed, restarting server...`
),
{ clear: true, timestamp: true }
);
server.restart();
}
});
}
};
}
async function getBuildId() {
return await new Promise((resolve, reject) => {
const git = spawn("git", ["rev-parse", "HEAD"], {
stdio: ["ignore", "pipe", "ignore"]
});
git.stdout.setEncoding("utf8");
let output = "";
git.stdout.on("data", (data) => {
output += data;
});
git.on("error", (err) => reject(err));
git.on("close", (code) => {
if (code === 0) {
resolve(output.trim().slice(0, 11));
} else {
reject(new Error());
}
});
}).catch(() => {
return Math.random().toString(36).substring(2, 15);
});
}
var cacheBuster = 0;
// src/vite-plugin/prevent-vite-build.ts
function preventViteBuild() {
let buildStepStartCalled = false;
let prevent = false;
return {
name: "rakkasjs:prevent-vite-build",
enforce: "pre",
apply: "build",
buildStepStart() {
buildStepStartCalled = true;
},
configResolved(config) {
if (config.buildSteps && config.mode !== "multibuild" && !buildStepStartCalled) {
prevent = true;
}
},
buildStart() {
if (prevent) {
throw new Error(
"rakkas: Please use the 'rakkas' command instead of 'vite build' to build a Rakkas project."
);
}
}
};
}
// src/vite-plugin/index.ts
import vaviteConnect from "@vavite/connect";
import exposeViteDevServer from "@vavite/expose-vite-dev-server";
// src/vite-plugin/resolve-client-manifest.ts
import fs from "fs";
import path2 from "path";
function resolveClientManifest() {
let resolvedConfig;
let dev = false;
return {
name: "rakkasjs:resolve-client-manifest",
enforce: "pre",
resolveId(id, _, options) {
if (id === "virtual:rakkasjs:client-manifest") {
if (dev || !options.ssr) {
return id;
} else {
return this.resolve(
path2.resolve(resolvedConfig.root, "dist/manifest.json")
);
}
}
},
load(id) {
if (id === "virtual:rakkasjs:client-manifest") {
return "export default undefined";
}
},
config(config, env) {
dev = env.command === "serve";
if (!config.build?.ssr) {
return {
build: {
manifest: true
}
};
}
},
configResolved(config) {
resolvedConfig = config;
},
async closeBundle() {
if (resolvedConfig.command === "serve" || resolvedConfig.build.ssr) {
return;
}
const from = path2.resolve(
resolvedConfig.root,
resolvedConfig.build.outDir,
"manifest.json"
);
await fs.promises.rename(from, resolvedConfig.root + "/dist/manifest.json").catch(() => {
});
}
};
}
// src/vite-plugin/virtual-default-entry.ts
import path3 from "path";
function virtualDefaultEntry(options) {
const { defaultContent, entry, virtualName, resolveName = true } = options;
let fallback;
return {
name: "rakkasjs:default-entry",
enforce: "pre",
async configResolved(config) {
if (resolveName) {
fallback = path3.resolve(config.root, entry.slice(1) + ".default.js").replace(/\\/g, "/");
} else {
fallback = "virtual:rakkasjs:" + virtualName;
}
},
async resolveId(id) {
if (id === "virtual:rakkasjs:" + virtualName || id === "/virtual:rakkasjs:" + virtualName || id === entry + ".default.js") {
const userEntry = await this.resolve(entry);
return userEntry ?? fallback;
}
},
async load(id) {
if (id === fallback) {
return defaultContent;
}
}
};
}
// src/features/api-routes/vite-plugin.ts
import micromatch2 from "micromatch";
import glob2 from "fast-glob";
import path4 from "path";
// src/internal/route-utils.ts
function routeToRegExp(route) {
route = route.replace(/\\/g, "/");
let restParamName;
const restMatch = route.match(/\/\[\.\.\.([a-zA-Z_][a-zA-Z0-9_]*)\]$/);
if (restMatch) {
const [rest, restName] = restMatch;
route = route.slice(0, -rest.length);
restParamName = restName;
}
return [
new RegExp(
"^" + route.split("/").filter((x) => x !== "index" && !x.startsWith("_")).map((x) => {
const escaped = encodeURIComponent(x);
return escaped.replace(/%5B/g, "[").replace(/%5D/g, "]");
}).join("/").replace(
// Escape special characters
/[\\^$*+?.()|[\]{}]/g,
(x) => `\\${x}`
).replace(
/\\\[[a-zA-Z_][a-zA-Z0-9_]*\\]/g,
(name) => `(?<${name.slice(2, -2)}>[^/]*)`
) + (restParamName ? `(?<${restParamName}>(\\/.*)?$)` : "\\/?$")
),
restParamName
];
}
function sortRoutes(routes) {
const processedRoutes1 = routes.map((route) => ({
original: route,
dynamicCount: route[0].match(/\[/g)?.length || 0,
isRest: !!route[0].match(/\/\[\.\.\.([a-zA-Z_][a-zA-Z0-9_]*)\]$/),
segments: route[0].split("/").filter((x) => !x.startsWith("_") && x !== "index").map((seg) => ({
content: seg,
paramCount: seg.split("[").length - 1
}))
}));
const processedRoutes = processedRoutes1.sort((a, b) => {
const restDiff = Number(a.isRest) - Number(b.isRest);
if (restDiff !== 0) {
return restDiff;
}
const dynamicOrder = a.dynamicCount - b.dynamicCount;
if (dynamicOrder)
return dynamicOrder;
const aSegments = a.segments;
const bSegments = b.segments;
for (let i = 0; i < aSegments.length; i++) {
const aSegment = aSegments[i];
const bSegment = bSegments[i];
const result = compareSegments(aSegment, bSegment);
if (result !== 0)
return result;
}
return 0;
});
return processedRoutes.map((route) => route.original);
}
function compareSegments(a, b) {
const definedOrder = Number(a === void 0) - Number(b === void 0);
if (definedOrder)
return definedOrder;
return a.paramCount - b.paramCount || // Alphabetical order
a.content.localeCompare(b.content);
}
// src/features/api-routes/vite-plugin.ts
function apiRoutes() {
const extPattern = "mjs|js|ts|jsx|tsx";
const endpointPattern = `/**/*.api.(${extPattern})`;
const middlewarePattern = `/**/middleware.(${extPattern})`;
let root;
let isMiddleware;
let isEndpoint;
let resolvedConfig;
function filterRoutes(filename) {
const configs = resolvedConfig.api?.rakkas?.routeConfigs || [];
const defaults = {};
for (const config of configs) {
if (filename.startsWith(config.dir)) {
if (config.value.disabled) {
return false;
}
Object.assign(defaults, config.value.defaults);
}
}
return !defaults.disabled;
}
function filter(fileNames) {
return filterRoutes ? fileNames.filter((f) => {
f = path4.relative(resolvedConfig.root, f).replace(/\\/g, "/").slice("src/routes/".length);
const filtered = filterRoutes(f);
return filtered;
}) : fileNames;
}
async function generateRoutesModule() {
const endpointFiles = filter(await glob2(root + endpointPattern));
const middlewareFiles = filter(
(await glob2(root + middlewarePattern)).sort(
/* Long to short */
(a, b) => b.length - a.length
)
);
const middlewareDirs = middlewareFiles.map(
(f, i) => [i, path4.dirname(f)]
);
let middlewareImporters = "";
for (const [i, middlewareFile] of middlewareFiles.entries()) {
middlewareImporters += `const m${i} = () => import(${JSON.stringify(
middlewareFile
)});
`;
}
let endpointImporters = "";
for (const [i, endpointFile] of endpointFiles.entries()) {
endpointImporters += `const e${i} = () => import(${JSON.stringify(
endpointFile
)});
`;
}
let exportStatement = "export default [\n";
const endpointRoutes = sortRoutes(
endpointFiles.map((endpointFile, i) => {
const relName = path4.relative(root, endpointFile);
const baseName = /^(.*)\.api\.(.*)$/.exec(relName)[1];
return [baseName, i, endpointFile];
})
);
for (const [baseName, i, endpointFile] of endpointRoutes) {
const middlewares = middlewareDirs.filter(([, dirName]) => endpointFile.startsWith(dirName + "/")).map(([mi]) => mi);
const [re, rest] = routeToRegExp("/" + baseName);
exportStatement += ` [${re}, [e${i}, ${middlewares.map(
(mi) => `m${mi}`
)}]${rest ? `, ${JSON.stringify(rest)}` : ""}],
`;
}
exportStatement += "]";
const out = [middlewareImporters, endpointImporters, exportStatement].filter(Boolean).join("\n");
return out;
}
return {
name: "rakkasjs:endpoint-router",
resolveId(id) {
if (id === "virtual:rakkasjs:api-routes") {
return id;
}
},
async load(id) {
if (id === "virtual:rakkasjs:api-routes") {
return generateRoutesModule();
}
},
configResolved(config) {
resolvedConfig = config;
root = config.root + "/src/routes";
isMiddleware = micromatch2.matcher(endpointPattern);
isEndpoint = micromatch2.matcher(middlewarePattern);
},
configureServer(server) {
server.watcher.addListener("all", async (e, fn) => {
if ((isEndpoint(fn) || isMiddleware(fn)) && (e === "add" || e === "unlink")) {
const module = server.moduleGraph.getModuleById(
"virtual:rakkasjs:api-routes"
);
if (module) {
server.moduleGraph.invalidateModule(module);
if (server.ws) {
server.ws.send({
type: "full-reload",
path: "*"
});
}
}
}
});
}
};
}
// src/features/pages/vite-plugin.ts
import micromatch3 from "micromatch";
import glob3 from "fast-glob";
import path5 from "path";
import MagicString from "magic-string";
function pageRoutes(options = {}) {
const { pageExtensions = ["tsx", "jsx"] } = options;
const extPattern = pageExtensions.join("|");
const pagePattern = `/**/*.page.(${extPattern})`;
const layoutPattern = `/**/layout.(${extPattern})`;
const jsPattern = "mjs|js|ts|jsx|tsx";
const guardPattern = `/**/$guard.(${jsPattern})`;
const singlePageGuardPattern = `/**/*.guard.(${jsPattern})`;
let resolvedConfig;
let routesRoot;
let isLayout;
let isPage;
let isGuard;
let isSinglePageGuard;
function getRenderModes(filename) {
const configs = resolvedConfig.api?.rakkas?.routeConfigs || [];
const defaults = {};
for (const config of configs) {
if (filename.startsWith(config.dir)) {
if (config.value.disabled) {
return false;
}
if (config.value.renderingMode) {
return config.value.renderingMode;
}
Object.assign(defaults, config.value.defaults);
}
}
return defaults.disabled === false ? false : defaults.renderingMode ?? "hydrate";
}
async function generateRoutesModule(client) {
const renderModes = /* @__PURE__ */ new Map();
const pageFiles = (await glob3(routesRoot + pagePattern)).map((f) => path5.relative(resolvedConfig.root, f).replace(/\\/g, "/")).filter((f) => {
f = f.slice("src/routes/".length);
const filtered = getRenderModes(f) ?? true;
if (typeof filtered === "string") {
renderModes.set(f, filtered);
}
return filtered;
});
let pageImporters = "";
for (const [i, pageFile] of pageFiles.entries()) {
pageImporters += `const p${i} = () => import(${JSON.stringify(
"/" + pageFile
)});
`;
}
let pageNames = "";
if (!client) {
for (const [i, pageFile] of pageFiles.entries()) {
pageNames += `const r${i} = ${JSON.stringify(pageFile)};
`;
}
}
const layoutFiles = (await glob3(routesRoot + layoutPattern)).sort(
/* Long to short */
(a, b) => b.length - a.length
).map((f) => path5.relative(resolvedConfig.root, f).replace(/\\/g, "/")).filter(getRenderModes);
const layoutDirs = layoutFiles.map((f) => path5.dirname(f));
let layoutImporters = "";
for (const [i, layoutFile] of layoutFiles.entries()) {
layoutImporters += `const l${i} = () => import(${JSON.stringify(
"/" + layoutFile
)});
`;
}
let layoutNames = "";
if (!client) {
for (const [i, layoutFile] of layoutFiles.entries()) {
layoutNames += `const m${i} = ${JSON.stringify(layoutFile)};
`;
}
}
const guardFiles = (await glob3(routesRoot + guardPattern)).sort(
/* short to kong */
(a, b) => a.length - b.length
).map((f) => path5.relative(resolvedConfig.root, f).replace(/\\/g, "/")).filter(getRenderModes);
const guardDirs = guardFiles.map((f) => path5.dirname(f));
let guardImporters = "";
for (const [i, guardFile] of guardFiles.entries()) {
guardImporters += `import { pageGuard as g${i} } from ${JSON.stringify(
"/" + guardFile
)};
`;
}
const singlePageGuardFiles = (await glob3(routesRoot + singlePageGuardPattern)).map((f) => path5.relative(resolvedConfig.root, f).replace(/\\/g, "/"));
let singlePageGuardImporters = "";
const guardedPageIndices = /* @__PURE__ */ new Set();
for (const [, singlePageGuardFile] of singlePageGuardFiles.entries()) {
const baseName = /^(.*)guard\.(.*)$/.exec(singlePageGuardFile)[1];
const pageIndex = pageFiles.findIndex((f) => f.startsWith(baseName));
if (pageIndex < 0)
continue;
singlePageGuardImporters += `import { pageGuard as s${pageIndex} } from ${JSON.stringify(
"/" + singlePageGuardFile
)};
`;
guardedPageIndices.add(pageIndex);
}
let exportStatement = "export default [\n";
const pageRoutes2 = sortRoutes(
pageFiles.map((endpointFile, i) => {
const relName = path5.relative(routesRoot, endpointFile).replace(/\\/g, "/");
const baseName = /^(.*)\.page\.(.*)$/.exec(relName)[1];
return [baseName, i, endpointFile];
})
);
for (const [baseName, i, pageFile] of pageRoutes2) {
const layouts = Array.from(layoutDirs.entries()).filter((entry) => pageFile.startsWith(entry[1] + "/")).map((entry) => entry[0]);
const guards = Array.from(guardDirs.entries()).filter((entry) => pageFile.startsWith(entry[1] + "/")).map((entry) => entry[0]);
const [re, rest] = routeToRegExp("/" + baseName);
let exportElement = ` [${re}, [p${i}, ${layouts.map(
(li) => `l${li}`
)}], [${guards.map((gi) => `g${gi}`)}`;
if (guardedPageIndices.has(i)) {
exportElement += guards.length ? `, s${i}` : `s${i}`;
}
exportElement += "]";
if (rest) {
exportElement += `, ${JSON.stringify(rest)}`;
} else {
exportElement += `, `;
}
if (!client) {
exportElement += `, [r${i}, ${layouts.map((li) => `m${li}`)}]`;
if (!client) {
const mode = renderModes.get(pageFile.slice("src/routes/".length)) ?? "hydrate";
exportElement += RENDER_MODES[mode] ?? "";
}
}
exportElement += "],\n";
exportStatement += exportElement;
}
exportStatement += "]";
const out = [
layoutImporters,
layoutNames,
pageImporters,
guardImporters,
singlePageGuardImporters,
pageNames,
exportStatement
].filter(Boolean).join("\n");
return out;
}
return [
{
name: "rakkasjs:page-router",
resolveId(id) {
if (id === "virtual:rakkasjs:server-page-routes") {
return id;
} else if (id.includes("virtual:rakkasjs:client-page-routes")) {
return "virtual:rakkasjs:client-page-routes";
}
},
async load(id, options2) {
if (id === "virtual:rakkasjs:server-page-routes") {
if (!options2?.ssr) {
return "export default null";
}
return generateRoutesModule();
} else if (id === "virtual:rakkasjs:client-page-routes") {
if (options2?.ssr) {
return "export default null";
}
return generateRoutesModule(true);
}
},
configResolved(config) {
resolvedConfig = config;
routesRoot = config.root + "/src/routes";
isLayout = micromatch3.matcher(
path5.resolve(routesRoot + "/" + layoutPattern).replace(/\\/g, "/")
);
isPage = micromatch3.matcher(
path5.resolve(routesRoot + "/" + pagePattern).replace(/\\/g, "/")
);
isGuard = micromatch3.matcher(
path5.resolve(routesRoot + "/" + guardPattern).replace(/\\/g, "/")
);
isSinglePageGuard = micromatch3.matcher(
path5.resolve(routesRoot + "/" + singlePageGuardPattern).replace(/\\/g, "/")
);
config.api.rakkas.isPage = isPage;
config.api.rakkas.isLayout = isLayout;
},
configureServer(server) {
server.watcher.addListener("all", async (e, fn) => {
const isGuardFile = isGuard(fn) || isSinglePageGuard(fn);
if ((isPage(fn) || isLayout(fn) || isGuardFile) && (e === "add" || e === "unlink") || isGuardFile && e === "change") {
const serverModule = server.moduleGraph.getModuleById(
"virtual:rakkasjs:server-page-routes"
);
const clientModule = server.moduleGraph.getModuleById(
"virtual:rakkasjs:client-page-routes"
);
if (serverModule) {
server.moduleGraph.invalidateModule(serverModule);
}
if (clientModule) {
server.moduleGraph.invalidateModule(clientModule);
}
if (server.ws && (serverModule || clientModule)) {
server.ws.send({
type: "full-reload",
path: "*"
});
}
}
});
},
transform(code, id, options2) {
if (options2?.ssr)
return;
if (isPage(id) || isLayout(id)) {
if (resolvedConfig.command === "serve" || resolvedConfig.build.sourcemap) {
const str = new MagicString(code);
str.append(PAGE_HOT_RELOAD);
return {
code: str.toString(),
map: str.generateMap({ hires: true })
};
} else {
return code + PAGE_HOT_RELOAD;
}
}
}
}
];
}
var PAGE_HOT_RELOAD = `
if (import.meta.hot) {
import.meta.hot.accept(() => {
$RAKKAS_UPDATE();
});
}
`;
var RENDER_MODES = {
server: ",1",
client: ",2"
};
// src/features/run-server-side/vite-plugin.ts
import { transformAsync } from "@babel/core";
// src/features/run-server-side/implementation/transform/transform-server-side.ts
import * as t2 from "@babel/types";
// src/features/run-server-side/implementation/transform/transform-utils.ts
import * as t from "@babel/types";
var RUN_SERVER_SIDE_FUNCTION_NAMES = [
"useServerSideQuery",
"useServerSideMutation",
"useServerSentEvents",
"useSSQ",
"useSSM",
"useSSE",
"runServerSideQuery",
"runServerSideMutation",
"runSSQ",
"runSSM",
"useFormMutation"
];
function isRunServerSideCall(expr, nameRef) {
if (!expr.isCallExpression()) {
return false;
}
const callee = expr.node.callee;
if (t.isIdentifier(callee)) {
const binding = expr.parentPath.scope.getBinding(callee.name);
nameRef.name = binding?.path.node?.imported?.name;
return !!(binding && binding.path.isImportSpecifier() && t.isIdentifier(binding.path.node.imported) && RUN_SERVER_SIDE_FUNCTION_NAMES.includes(
binding.path.node.imported.name
) && binding.path.parentPath.isImportDeclaration() && binding.path.parentPath.node.source.value === "rakkasjs");
} else if (t.isMemberExpression(callee)) {
if (!t.isIdentifier(callee.object)) {
return false;
}
const binding = expr.parentPath.scope.getBinding(callee.object.name);
nameRef.name = callee.property?.name;
return !!(binding && (binding.path.isImportDefaultSpecifier() || binding.path.isImportNamespaceSpecifier()) && binding.path.parentPath.isImportDeclaration() && binding.path.parentPath.node.source.value === "rakkasjs" && t.isIdentifier(callee.property) && RUN_SERVER_SIDE_FUNCTION_NAMES.includes(callee.property.name));
}
return false;
}
function getAlreadyUnreferenced(program) {
const alreadyUnreferenced = /* @__PURE__ */ new Set();
for (const [name, binding] of Object.entries(program.scope.bindings)) {
if (!binding.referenced) {
alreadyUnreferenced.add(name);
}
}
return alreadyUnreferenced;
}
function removeUnreferenced(program, alreadyUnreferenced) {
for (; ; ) {
program.scope.crawl();
let removed = false;
for (const [name, binding] of Object.entries(program.scope.bindings)) {
if (binding.referenced || alreadyUnreferenced.has(name)) {
continue;
}
const parent = binding.path.parentPath;
if (parent?.isImportDeclaration() && parent.node.specifiers.length === 1) {
parent.remove();
} else {
binding.path.remove();
}
removed = true;
}
if (!removed)
break;
}
}
// src/features/run-server-side/implementation/transform/transform-server-side.ts
function babelTransformServerSideHooks(moduleId) {
let counter = 0;
return {
visitor: {
Program: {
exit(program) {
const alreadyUnreferenced = getAlreadyUnreferenced(program);
const hoisted = [];
program.traverse({
CallExpression: {
exit(call) {
const nameRef = {};
if (!isRunServerSideCall(call, nameRef)) {
return;
}
const argNo = nameRef.name === "runSSQ" || nameRef.name === "runServerSideQuery" ? 1 : 0;
let fn = call.get(`arguments.${argNo}`);
if (!t2.isArrowFunctionExpression(fn) && !t2.isFunctionExpression(fn)) {
fn = fn.replaceWith(
t2.arrowFunctionExpression(
[t2.restElement(t2.identifier("$runServerSideArgs$"))],
t2.callExpression(fn.node, [
t2.spreadElement(t2.identifier("$runServerSideArgs$"))
])
)
)[0];
}
let body = fn.get("body");
const identifiers = /* @__PURE__ */ new Set();
if (body.type !== "BlockStatement") {
body = body.replaceWith(
t2.blockStatement([
t2.returnStatement(body.node)
])
)[0];
fn.scope.parent.crawl();
}
body.traverse({
Identifier: {
exit(identifier3) {
const binding = fn.scope.parent.getBinding(
identifier3.node.name
);
if (program.scope.getBinding(identifier3.node.name)?.referencePaths.includes(identifier3)) {
return;
}
if (binding?.path.get("id") === identifier3 || binding?.referencePaths.includes(identifier3)) {
identifiers.add(identifier3.node.name);
}
}
}
});
const ids = [...identifiers];
const replacement = t2.arrayExpression([
t2.stringLiteral(moduleId),
t2.numericLiteral(counter),
t2.arrayExpression(ids.map((id) => t2.identifier(id))),
t2.memberExpression(
t2.identifier("$runServerSide$"),
t2.numericLiteral(counter++),
true
)
]);
if (t2.isArrowFunctionExpression(fn.node) || t2.isFunctionExpression(fn.node)) {
fn.node.async = true;
fn.node.params.unshift(
t2.identifier("$runServerSideClosure$")
);
if (t2.isExpression(fn.node.body)) {
fn.node.body = t2.blockStatement([
t2.returnStatement(fn.node.body)
]);
}
fn.node.body.body.unshift(
t2.variableDeclaration("let", [
t2.variableDeclarator(
t2.arrayPattern(ids.map((id) => t2.identifier(id))),
t2.identifier("$runServerSideClosure$")
)
])
);
}
hoisted.push(fn.node);
fn.replaceWith(replacement);
if (nameRef.name === "runSSM" || nameRef.name === "runServerSideMutation") {
call.parentPath.replaceWith(t2.nullLiteral());
return;
}
}
}
});
if (hoisted.length) {
program.node.body.push(
t2.exportNamedDeclaration(
t2.variableDeclaration("const", [
t2.variableDeclarator(
t2.identifier("$runServerSide$"),
t2.arrayExpression(hoisted)
)
])
)
);
}
removeUnreferenced(program, alreadyUnreferenced);
}
}
}
};
}
// src/features/run-server-side/implementation/transform/transform-client-side.ts
import * as t3 from "@babel/types";
function babelTransformClientSideHooks(moduleId, modifiedRef) {
return {
visitor: {
Program: {
exit(program) {
let counter = 0;
const alreadyUnreferenced = getAlreadyUnreferenced(program);
program.traverse({
CallExpression: {
exit(call) {
const nameRef = {};
if (!isRunServerSideCall(call, nameRef)) {
return;
}
const argNo = nameRef.name === "runSSQ" || nameRef.name === "runServerSideQuery" ? 1 : 0;
let fn = call.get(`arguments.${argNo}`);
if (!t3.isArrowFunctionExpression(fn) && !t3.isFunctionExpression(fn)) {
fn = fn.replaceWith(
t3.arrowFunctionExpression(
[t3.restElement(t3.identifier("$runServerSideArgs$"))],
t3.callExpression(fn.node, [
t3.spreadElement(t3.identifier("$runServerSideArgs$"))
])
)
)[0];
}
let body = fn.get("body");
const identifiers = /* @__PURE__ */ new Set();
if (body.type !== "BlockStatement") {
body = body.replaceWith(
t3.blockStatement([
t3.returnStatement(body.node)
])
)[0];
fn.scope.parent.crawl();
}
body.traverse({
Identifier: {
exit(identifier3) {
const binding = fn.scope.parent.getBinding(
identifier3.node.name
);
if (program.scope.getBinding(identifier3.node.name)?.referencePaths.includes(identifier3)) {
return;
}
if (binding?.path.get("id") === identifier3 || binding?.referencePaths.includes(identifier3)) {
identifiers.add(identifier3.node.name);
}
}
}
});
modifiedRef.current = true;
fn.replaceWith(
t3.arrayExpression([
t3.stringLiteral(moduleId),
t3.numericLiteral(counter++),
t3.arrayExpression(
[...identifiers].map((id) => t3.identifier(id))
)
])
);
}
}
});
if (!modifiedRef.current) {
return;
}
removeUnreferenced(program, alreadyUnreferenced);
}
}
}
};
}
// src/features/run-server-side/vite-plugin.ts
function runServerSide() {
let idCounter = 0;
const moduleIdMap = {};
let resolvedConfig;
let moduleManifest;
return [
{
name: "rakkasjs:run-server-side:manifest",
enforce: "pre",
config() {
return {
ssr: {
noExternal: ["virtual:rakkasjs:run-server-side:manifest"]
}
};
},
resolveId(id) {
if (id === "virtual:rakkasjs:run-server-side:manifest") {
return id;
}
},
async load(id) {
if (id === "virtual:rakkasjs:run-server-side:manifest") {
if (resolvedConfig.command === "serve") {
return `export default new Proxy({}, { get: (_, name) => () => import(/* @vite-ignore */ "/" + name) });`;
} else if (!moduleManifest) {
return `throw new Error("[virtual:rakkasjs:run-server-side:manifest]: Module manifest is not available on the client");`;
}
let code = "export default {";
for (const [filePath, moduleId] of Object.entries(moduleManifest)) {
code += `
${JSON.stringify(
moduleId
)}: () => import(${JSON.stringify("/" + filePath)}),`;
}
code += "\n};";
return code;
}
}
},
{
name: "rakkasjs:run-server-side:transform",
enforce: "post",
configResolved(config) {
resolvedConfig = config;
},
async transform(code, id, options) {
const plugins = [];
const ref = { current: false };
let moduleId;
if (id.startsWith(resolvedConfig.root) && code.match(
/\buseServerSideQuery|useServerSentEvents|useServerSideMutation|useSSQ|useSSM|useSSE|runServerSideQuery|runServerSideMutation|runSSQ|runSSM|useFormMutation\b/
) && code.includes(`"rakkasjs"`) && !code.includes(`'rakkasjs'`)) {
if (resolvedConfig.command === "serve") {
moduleId = id.slice(resolvedConfig.root.length + 1);
} else if (moduleManifest) {
moduleId = moduleManifest[id];
} else {
moduleId = (idCounter++).toString(36);
}
plugins.push(
options?.ssr ? babelTransformServerSideHooks(moduleId) : babelTransformClientSideHooks(moduleId, ref)
);
}
if (!plugins.length) {
return;
}
const result = await transformAsync(code, {
filename: id,
code: true,
plugins,
sourceMaps: resolvedConfig.command === "serve" || !!resolvedConfig.build.sourcemap
});
if (ref.current) {
moduleIdMap[id] = moduleId;
}
if (result) {
return {
code: result.code,
map: result.map
};
} else {
this.warn(`[rakkasjs:run-server-side]: Failed to transform ${id}`);
}
},
buildStepStart(_info, forwarded) {
moduleManifest = forwarded;
},
buildStepEnd() {
return moduleIdMap;
}
}
];
}
// src/vite-plugin/adapters.ts
import path6 from "path";
import fs2 from "fs";
import cloudflareWorkers from "@hattip/bundler-cloudflare-workers";
import { bundle as netlify } from "@hattip/bundler-netlify";
import { bundle as vercel } from "@hattip/bundler-vercel";
import deno from "@hattip/bundler-deno";
var adapters = {
node: {
name: "node"
},
"cloudflare-workers": {
name: "cloudflare-workers",
async bundle(root) {
let entry = findEntry(root, "src/entry-cloudflare-workers");
if (!entry) {
entry = path6.resolve(root, "dist/server/entry-cloudflare-workers.js");
await fs2.promises.writeFile(entry, CLOUDFLARE_WORKERS_ENTRY);
}
cloudflareWorkers(
{
output: path6.resolve(
root,
"dist/server/cloudflare-workers-bundle.js"
),
cfwEntry: entry
},
(options) => {
options.define = options.define || {};
options.define["process.env.RAKKAS_PRERENDER"] = "undefined";
options.define["global"] = "globalThis";
}
);
}
},
vercel: {
name: "vercel",
disableStreaming: true,
async bundle(root) {
let entry = findEntry(root, "src/entry-vercel");
if (!entry) {
entry = path6.resolve(root, "dist/server/entry-vercel.js");
await fs2.promises.writeFile(entry, VERCEL_ENTRY);
}
vercel({
serverlessEntry: entry,
staticDir: path6.resolve(root, "dist/client"),
manipulateEsbuildOptions(options) {
options.define = options.define || {};
options.define["process.env.NODE_ENV"] = '"production"';
options.define["process.env.RAKKAS_PRERENDER"] = "undefined";
}
});
}
},
"vercel-edge": {
name: "vercel-edge",
async bundle(root) {
let entry = findEntry(root, "src/entry-vercel-edge");
if (!entry) {
entry = path6.resolve(root, "dist/server/entry-vercel-edge.js");
await fs2.promises.writeFile(entry, VERCEL_EDGE_ENTRY);
}
vercel({
edgeEntry: entry,
staticDir: path6.resolve(root, "dist/client"),
manipulateEsbuildOptions(options) {
options.define = options.define || {};
options.define["process.env.RAKKAS_PRERENDER"] = "undefined";
options.define["global"] = "globalThis";
}
});
}
},
netlify: {
name: "netlify",
disableStreaming: true,
async bundle(root) {
let entry = findEntry(root, "src/entry-netlify");
if (!entry) {
entry = path6.resolve(root, "dist/server/entry-netlify.js");
await fs2.promises.writeFile(entry, NETLIFY_ENTRY);
}
netlify({
functionEntry: entry,
staticDir: path6.resolve(root, "dist/client"),
manipulateEsbuildOptions(options) {
options.define = options.define || {};
options.define["process.env.NODE_ENV"] = '"production"';
options.define["process.env.RAKKAS_PRERENDER"] = "undefined";
}
});
}
},
"netlify-edge": {
name: "netlify-edge",
async bundle(root) {
let entry = findEntry(root, "src/entry-netlify-edge");
if (!entry) {
entry = path6.resolve(root, "dist/server/entry-netlify-edge.js");
await fs2.promises.writeFile(entry, NETLIFY_EDGE_ENTRY);
}
await generateStaticAssetManifest(root);
netlify({
edgeEntry: entry,
staticDir: path6.resolve(root, "dist/client"),
manipulateEsbuildOptions(options) {
options.define = options.define || {};
options.define["process.env.RAKKAS_PRERENDER"] = "undefined";
options.define["global"] = "globalThis";
}
});
}
},
deno: {
name: "deno",
async bundle(root) {
let input = findEntry(root, "src/entry-deno");
if (!input) {
input = path6.resolve(root, "dist/server/entry-deno.js");
await fs2.promises.writeFile(input, DENO_ENTRY);
}
await generateStaticAssetManifest(root);
deno(
{
input,
output: path6.resolve(root, "dist/deno/mod.js"),
staticDir: "dist/client"
},
(options) => {
options.define = options.define || {};
options.define["process.env.NODE_ENV"] = '"production"';
options.define["process.env.RAKKAS_PRERENDER"] = "undefined";
options.define["global"] = "globalThis";
}
);
}
},
bun: {
name: "bun",
disableStreaming: true,
async bundle(root) {
let input = findEntry(root, "src/entry-bun");
if (!input) {
input = path6.resolve(root, "dist/server/entry-bun.js");
await fs2.promises.writeFile(input, BUN_ENTRY);
}
}
},
lagon: {
name: "lagon",
disableStreaming: true,
async bundle(root) {
let input = findEntry(root, "src/entry-lagon");
if (!input) {
input = path6.resolve(root, "dist/server/entry-lagon.js");
await fs2.promises.writeFile(input, LAGON_ENTRY);
}
}
}
};
function findEntry(root, name) {
const entries = [
path6.resolve(root, name) + ".ts",
path6.resolve(root, name) + ".js",
path6.resolve(root, name) + ".tsx",
path6.resolve(root, name) + ".jsx"
];
return entries.find((entry) => fs2.existsSync(entry));
}
async function generateStaticAssetManifest(root) {
const files = walk(path6.resolve(root, "dist/client"));
await fs2.promises.writeFile(
path6.resolve(root, "dist/server/static-manifest.js"),
`export default new Set(${JSON.stringify([...files])})`
);
}
function walk(dir, root = dir, entries = /* @__PURE__ */ new Set()) {
const files = fs2.readdirSync(dir);
for (const file of files) {
const filepath = path6.join(dir, file);
const stat = fs2.statSync(filepath);
if (stat.isDirectory()) {
walk(filepath, root, entries);
} else {
entries.add("/" + path6.relative(root, filepath).replace(/\\/g, "/"));
}
}
return entries;
}
var CLOUDFLARE_WORKERS_ENTRY = `
import cloudflareWorkersAdapter from "@hattip/adapter-cloudflare-workers";
let handler;
export default {
async fetch(req, env, ctx) {
if (!globalThis.process?.env) {
globalThis.process = globalThis.process || {};
globalThis.process.env = new Proxy({}, {
get(_, key) {
if (typeof env[key] === "string") {
return env[key];
}
return undefined;
}
});
}
if (!handler) {
const hattipHandler = await import("./hattip.js");
handler = cloudflareWorkersAdapter(hattipHandler.default);
}
return handler(req, env, ctx);
}
};
`;
var NETLIFY_ENTRY = `
import adapter from "@hattip/adapter-netlify-functions";
import hattipHandler from "./hattip.js";
export const handler = adapter(hattipHandler);
`;
var NETLIFY_EDGE_ENTRY = `
import adapter from "@hattip/adapter-netlify-edge";
import staticFiles from "./static-manifest.js";
export default adapter(async (ctx) => {
globalThis.process = { env: Deno.env.toObject() };
const path = new URL(ctx.request.url).pathname;
if (staticFiles.has(path) || staticFiles.has(path + "/index.html")) {
ctx.passThrough();
return new Response("", { status: 404 });
}
const handler = await import("./hattip.js");
return handler.default(ctx);
});
`;
var VERCEL_ENTRY = `
import { createMiddleware } from "rakkasjs/node-adapter";
import handler from "./hattip.js";
export default createMiddleware(handler, { origin: "", trustProxy: true });
`;
var VERCEL_EDGE_ENTRY = `
import { ReadableStream } from 'web-streams-polyfill/ponyfill';
Object.assign(globalThis, { ReadableStream });
import adapter from "@hattip/adapter-vercel-edge";
export default adapter(async ctx => {
const handler = await import("./hattip.js");
return handler.default(ctx);
});
`;
var DENO_ENTRY = `
import * as path from "https://deno.land/std@0.144.0/path/mod.ts";
import { serve, serveDir, createRequestHandler } from "@hattip/adapter-deno";
import handler from "./hattip.js";
import staticFiles from "./static-manifest.js";
const staticDir = path.join(path.dirname(path.fromFileUrl(import.meta.url)), "public");
const denoHandler = createRequestHandler(handler);
serve(
async (request, connInfo) => {
const url = new URL(request.url);
const path = url.pathname;
if (staticFiles.has(path)) {
return serveDir(request, { fsRoot: staticDir });
} else if (staticFiles.has(path + "/index.html")) {
url.pathname = path + "/index.html";
return serveDir(new Request(url, request), { fsRoot: staticDir });
}
return denoHandler(request, connInfo);
},
{
port: Number(process.env.PORT) || 3000,
},
);
`;
var BUN_ENTRY = `
import bunAdapter from "@hattip/adapter-bun";
import handler from "./hattip.js";
import url from "url";
import path from "path";
Request.prototype.formData = async function () {
return new URLSearchParams(await this.text());
};
const dir = path.resolve(
path.dirname(url.fileURLToPath(new URL(import.meta.url))),
"../client",
);
export default bunAdapter(handler, { staticDir: dir });
`;
var LAGON_ENTRY = `
import lagonAdapter from "@hattip/adapter-lagon";
import hattipHandler from "./hattip.js";
const originalFormData = Request.prototype.formData;
Request.prototype.formData = async function () {
if (this.headers.get("content-type")?.startsWith("multipart/form-data")) {
return originalFormData.call(this);
} else {
return new URLSearchParams(await this.text());
}
};
export const handler = lagonAdapter(hattipHandler);
`;
// src/features/run-server-side/implementation/transform/transform-client-page.ts
import * as t4 from "@babel/types";
function babelTransformClientSidePages() {
return {
visitor: {
Program: {
exit(program) {
const alreadyUnreferenced = getAlreadyUnreferenced(program);
let modified = false;
program.traverse({
ExportNamedDeclaration: {
enter(path7) {
if (t4.isFunctionDeclaration(path7.node.declaration) && SSR_EXPORTS.includes(path7.node.declaration.id.name)) {
path7.remove();
modified = true;
} else if (t4.isVariableDeclaration(path7.node.declaration)) {
const declarations = path7.get("declaration").get("declarations");
for (const declaration of declarations) {
if (t4.isIdentifier(declaration.node.id) && SSR_EXPORTS.includes(declaration.node.id.name)) {
declaration.remove();
modified = true;
}
}
} else if (path7.node.specifiers.length) {
const specifiers = path7.get("specifiers");
for (const specifier of specifiers) {
if (t4.isExportSpecifier(specifier) && t4.isIdentifier(specifier.node.exported) && SSR_EXPORTS.includes(specifier.node.exported.name)) {
specifier.remove();
modified = true;
}
}
}
}
}
});
if (modified) {
removeUnreferenced(program, alreadyUnreferenced);
}
}
}
}
};
}
var SSR_EXPORTS = ["headers", "prerender", "action"];
// src/vite-plugin/index.ts
function rakkas(options = {}) {
let { prerender = [], adapter = "node" } = options;
if (prerender === true) {
prerender = ["/"];
} else if (prerender === false) {
prerender = [];
}
if (typeof adapter === "string") {
adapter = adapters[adapter];
}
let resolvedConfig;
return [
...vaviteConnect({
handlerEntry: "/virtual:rakkasjs:node-entry",
clientAssetsDir: "dist/client",
serveClientAssetsInDev: true
}),
exposeViteDevServer(),
preventViteBuild(),
injectConfig({
prerender,
adapter,
strictMode: options.strictMode ?? true
}),
apiRoutes(),
pageRoutes({
pageExtensions: options.pageExtensions
}),
virtualDefaultEntry({
entry: "/src/entry-node",
virtualName: "node-entry",
defaultContent: DEFAULT_NODE_ENTRY_CONTENTS
}),
virtualDefaultEntry({
entry: "/src/entry-hattip",
virtualName: "hattip-entry",
defaultContent: DEFAULT_HATTIP_ENTRY_CONTENTS
}),
virtualDefaultEntry({
entry: "/src/entry-client",
virtualName: "client-entry",
defaultContent: DEFAULT_CLIENT_ENTRY_CONTENTS,
resolveName: false
}),
virtualDefaultEntry({
entry: "/src/common-hooks",
virtualName: "common-hooks",
defaultContent: DEFAULT_COMMON_HOOKS_CONTENTS,
resolveName: false
}),
virtualDefaultEntry