UNPKG

rwsdk

Version:

Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime

148 lines (147 loc) 7.65 kB
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import os from "os"; import path from "path"; import { VENDOR_CLIENT_BARREL_EXPORT_PATH, VENDOR_CLIENT_BARREL_PATH, VENDOR_SERVER_BARREL_EXPORT_PATH, VENDOR_SERVER_BARREL_PATH, } from "../lib/constants.mjs"; import { normalizeModulePath } from "../lib/normalizeModulePath.mjs"; import { runDirectivesScan } from "./runDirectivesScan.mjs"; export const generateVendorBarrelContent = (files, projectRootDir) => { const imports = [...files] .filter((file) => file.includes("node_modules")) .map((file, i) => `import * as M${i} from '${normalizeModulePath(file, projectRootDir, { absolute: true, })}';`) .join("\n"); const exports = "export default {\n" + [...files] .filter((file) => file.includes("node_modules")) .map((file, i) => ` '${normalizeModulePath(file, projectRootDir)}': M${i},`) .join("\n") + "\n};"; return `${imports}\n\n${exports}`; }; export const generateAppBarrelContent = (files, projectRootDir) => { return [...files] .filter((file) => !file.includes("node_modules")) .map((file) => { const resolvedPath = normalizeModulePath(file, projectRootDir, { absolute: true, }); return `import "${resolvedPath}";`; }) .join("\n"); }; export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRootDir, workerEntryPathname, }) => { const { promise: scanPromise, resolve: resolveScanPromise } = Promise.withResolvers(); const tempDir = mkdtempSync(path.join(os.tmpdir(), "rwsdk-")); const APP_CLIENT_BARREL_PATH = path.join(tempDir, "app-client-barrel.js"); const APP_SERVER_BARREL_PATH = path.join(tempDir, "app-server-barrel.js"); return { name: "rwsdk:directive-modules-dev", configureServer(server) { if (!process.env.VITE_IS_DEV_SERVER) { resolveScanPromise(); return; } runDirectivesScan({ rootConfig: server.config, environments: server.environments, clientFiles, serverFiles, entries: [workerEntryPathname], }).then(() => { // context(justinvdm, 11 Sep 2025): For vendor barrels, we write the // files directly to disk after the scan. For app barrels, we use a // more complex esbuild plugin to provide content in-memory. This is // because app barrels require special handling to prevent Vite from // marking application code as `external: true`. Vendor barrels do not // have this requirement and a simpler, direct-write approach is more // stable. const vendorClientBarrelContent = generateVendorBarrelContent(clientFiles, projectRootDir); writeFileSync(VENDOR_CLIENT_BARREL_PATH, vendorClientBarrelContent); const vendorServerBarrelContent = generateVendorBarrelContent(serverFiles, projectRootDir); writeFileSync(VENDOR_SERVER_BARREL_PATH, vendorServerBarrelContent); resolveScanPromise(); }); server.middlewares.use(async (_req, _res, next) => { await scanPromise; next(); }); }, configResolved(config) { if (config.command !== "serve") { return; } mkdirSync(path.dirname(APP_CLIENT_BARREL_PATH), { recursive: true }); writeFileSync(APP_CLIENT_BARREL_PATH, ""); mkdirSync(path.dirname(APP_SERVER_BARREL_PATH), { recursive: true }); writeFileSync(APP_SERVER_BARREL_PATH, ""); mkdirSync(path.dirname(VENDOR_CLIENT_BARREL_PATH), { recursive: true }); writeFileSync(VENDOR_CLIENT_BARREL_PATH, ""); mkdirSync(path.dirname(VENDOR_SERVER_BARREL_PATH), { recursive: true }); writeFileSync(VENDOR_SERVER_BARREL_PATH, ""); for (const [envName, env] of Object.entries(config.environments || {})) { env.optimizeDeps ??= {}; env.optimizeDeps.include ??= []; const entries = (env.optimizeDeps.entries = castArray(env.optimizeDeps.entries ?? [])); env.optimizeDeps.include.push(VENDOR_CLIENT_BARREL_EXPORT_PATH, VENDOR_SERVER_BARREL_EXPORT_PATH); if (envName === "client" || envName === "ssr") { entries.push(APP_CLIENT_BARREL_PATH); } else if (envName === "worker") { entries.push(APP_SERVER_BARREL_PATH); } env.optimizeDeps.esbuildOptions ??= {}; env.optimizeDeps.esbuildOptions.plugins ??= []; env.optimizeDeps.esbuildOptions.plugins.unshift({ name: "rwsdk:app-barrel-blocker", setup(build) { const appBarrelPaths = [ APP_CLIENT_BARREL_PATH, APP_SERVER_BARREL_PATH, ]; const appBarrelFilter = new RegExp(`(${appBarrelPaths .map((p) => p.replace(/\\/g, "\\\\")) .join("|")})$`); build.onResolve({ filter: /.*/ }, async (args) => { // Block all resolutions until the scan is complete. await scanPromise; // Handle app barrel files if (appBarrelFilter.test(args.path)) { return { path: args.path, namespace: "rwsdk-app-barrel-ns", }; } // context(justinvdm, 11 Sep 2025): Prevent Vite from // externalizing our application files. If we don't, paths // imported in our application barrel files will be marked as // external, and thus not scanned for dependencies. if (args.path.startsWith("/") && (args.path.includes("/src/") || args.path.includes("/generated/")) && !args.path.includes("node_modules")) { // By returning a result, we claim the module and prevent vite:dep-scan // from marking it as external. return { path: args.path, }; } }); build.onLoad({ filter: /.*/, namespace: "rwsdk-app-barrel-ns" }, (args) => { const isServerBarrel = args.path.includes("app-server-barrel"); const files = isServerBarrel ? serverFiles : clientFiles; const content = generateAppBarrelContent(files, projectRootDir); return { contents: content, loader: "js", }; }); }, }); } }, }; }; const castArray = (value) => { return Array.isArray(value) ? value : [value]; };