UNPKG

rakkasjs

Version:

Bleeding-edge React framework powered by Vite

1,514 lines (1,476 loc) 51.5 kB
// 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