rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
161 lines (160 loc) • 7.47 kB
JavaScript
import debug from "debug";
import MagicString from "magic-string";
import path from "path";
import { VENDOR_CLIENT_BARREL_EXPORT_PATH, VENDOR_SERVER_BARREL_EXPORT_PATH, } from "../lib/constants.mjs";
export function generateLookupMap({ files, isDev, kind, exportName, }) {
const s = new MagicString(`
export const ${exportName} = {
${Array.from(files)
.map((file) => {
if (file.includes("node_modules") && isDev) {
const barrelPath = kind === "client"
? VENDOR_CLIENT_BARREL_EXPORT_PATH
: VENDOR_SERVER_BARREL_EXPORT_PATH;
return `
"${file}": () => import("${barrelPath}").then(m => m.default["${file}"]),
`;
}
else {
return `
"${file}": () => import("${file}"),
`;
}
})
.join("")}
};
`);
return {
code: s.toString(),
map: s.generateMap(),
};
}
export const createDirectiveLookupPlugin = async ({ projectRootDir, files, config, }) => {
const debugNamespace = `rwsdk:vite:${config.pluginName}`;
const log = debug(debugNamespace);
let isDev = false;
let devServer;
log("Initializing %s plugin with projectRootDir=%s", config.pluginName, projectRootDir);
return {
name: `rwsdk:${config.pluginName}`,
config(_, { command, isPreview }) {
isDev = !isPreview && command === "serve";
log("Development mode: %s", isDev);
},
configureServer(server) {
devServer = server;
},
configEnvironment(env, viteConfig) {
if (!isDev &&
process.env.RWSDK_BUILD_PASS &&
process.env.RWSDK_BUILD_PASS !== "worker") {
return;
}
log("Configuring environment: env=%s", env);
viteConfig.optimizeDeps ??= {};
viteConfig.optimizeDeps.esbuildOptions ??= {};
viteConfig.optimizeDeps.esbuildOptions.plugins ??= [];
viteConfig.optimizeDeps.esbuildOptions.plugins.push({
name: `rwsdk:${config.pluginName}`,
setup(build) {
log("Setting up esbuild plugin for %s", config.virtualModuleName);
// Handle both direct virtual module name and /@id/ prefixed version
const escapedVirtualModuleName = config.virtualModuleName.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
const escapedPrefixedModuleName = `/@id/${config.virtualModuleName}`.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
build.onResolve({
filter: new RegExp(`^(${escapedVirtualModuleName}|${escapedPrefixedModuleName})\\.js$`),
}, () => {
process.env.VERBOSE &&
log("Esbuild onResolve: marking %s as external", config.virtualModuleName);
return {
path: `${config.virtualModuleName}.js`,
external: true,
};
});
},
});
const shouldOptimizeForEnv = !config.optimizeForEnvironments ||
config.optimizeForEnvironments.includes(env);
if (shouldOptimizeForEnv) {
log("Applying optimizeDeps and aliasing for environment: %s", env);
viteConfig.optimizeDeps.include ??= [];
for (const file of files) {
if (file.includes("node_modules")) {
continue;
}
const actualFilePath = path.join(projectRootDir, file);
process.env.VERBOSE &&
log("Adding to optimizeDeps.entries: %s", actualFilePath);
const entries = Array.isArray(viteConfig.optimizeDeps.entries)
? viteConfig.optimizeDeps.entries
: [].concat(viteConfig.optimizeDeps.entries ?? []);
viteConfig.optimizeDeps.entries = entries;
entries.push(actualFilePath);
}
log("Environment configuration complete for env=%s", env);
}
else {
log("Skipping optimizeDeps and aliasing for environment: %s", env);
}
},
resolveId(source) {
// Skip during directive scanning to avoid performance issues
if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
return;
}
if (source !== `${config.virtualModuleName}.js`) {
return null;
}
// context(justinvdm, 3 Sep 2025): This logic determines *when* to
// generate and bundle the lookup map. By conditionally externalizing it,
// we ensure the map is only created after tree-shaking is complete and
// that it's correctly shared between the SSR and final worker builds.
log("Resolving %s module", config.virtualModuleName);
const envName = this.environment?.name;
// 1. Worker Pass -> externalize
if (isDev &&
envName === "worker" &&
process.env.RWSDK_BUILD_PASS === "worker") {
// context(justinvdm, 3 Sep 2025): We externalize the lookup during the
// first worker pass. This defers its bundling until after the
// directivesFilteringPlugin has had a chance to run and tree-shake
// the list of client/server files.
log("Marking as external for worker pass");
return { id: source, external: true };
}
// 2. SSR Pass -> externalize
if (isDev && envName === "ssr") {
// context(justinvdm, 3 Sep 2025): We also externalize during the SSR
// build. This ensures that both the worker and SSR artifacts refer to
// the same virtual module, which will be resolved into a single, shared
// lookup map during the final linker pass.
log("Marking as external for ssr pass");
return { id: source, external: true };
}
// 3. Client Pass & 4. Linker Pass -> resolve and bundle
// context(justinvdm, 3 Sep 2025): For the client build, the dev server,
// and the final linker pass, we resolve the module ID with a null-byte
// prefix. This signals to Vite/Rollup that this is a virtual module
// whose content should be provided by the `load` hook, bundling it in.
log("Resolving for bundling");
return source;
},
async load(id) {
// Skip during directive scanning to avoid performance issues
if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
return;
}
if (id === config.virtualModuleName + ".js") {
log("Loading %s module with %d files", config.virtualModuleName, files.size);
const environment = this.environment?.name || "client";
log("Current environment: %s, isDev: %s", environment, isDev);
return generateLookupMap({
files,
isDev,
kind: config.kind,
exportName: config.exportName,
});
}
},
};
};