Server Functions Loader & Plugin
Analyze the inner workings of Webpack's module transforms for parsing Server Functions and compiling secure whitelist manifests.
Key Files Location:
• Loader transform:./dinou/webpack/loaders/server-functions-loader.js
• Aggregation plugin:./dinou/webpack/plugins/server-functions-plugin.js
💡 Overview
Server Functions represent sensitive backend endpoints executing database queries or handling credentials. Webpack compiles client bundles, meaning server components code must be stripped and replaced with client-side fetch proxies.
Dinou achieves this using a custom Webpack loader to replace modules containing "use server" with fetch skeletons, and a custom compilation plugin to aggregate the exported functions into server-functions-manifest.json.
🤝 The Teamwork Lifecycle (Step-by-Step)
The loader and the plugin coordinate in a two-stage relay race to compile Server Functions safely:
- Loader (Stage 1 - File-by-File):
- Intercepts individual module source files containing the
"use server"directive. - Discards all server-side implementation logic (preventing leakage into client bundles).
- Replaces exports with dynamic fetch proxy stubs that point to a temporary
"__SERVER_FUNCTION_PROXY__"token. - Calls Webpack's
this.emitFileto output a temporary file underserver-functions/[path].jsoncontaining the function whitelists.
- Intercepts individual module source files containing the
- Plugin (Stage 2 - Compilation Wrap):
- Hooks into Webpack's
PROCESS_ASSETS_STAGE_REPORTphase after all bundles are grouped. - Scans JavaScript chunks and replaces the temporary
"__SERVER_FUNCTION_PROXY__"string with the actual hashed proxy script URL. - Crawls the emitted
server-functions/*.jsonfiles, aggregates the whitelisted exports into a singleserver-functions-manifest.jsonon disk, and deletes the temporary JSON files so they are not written to production outputs.
- Hooks into Webpack's
📊 Server Functions Flow
The flowchart below shows how Server Functions are stripped, proxied, and whitelisted during Webpack compiles:
⚙️ server-functions-loader.js Walkthrough
Below is the full code of the custom loader that strips code bodies and generates fetch proxies:
const path = require("path");
const { normalizePathCase } = require("../../core/path-utils.js");
const parseExports = require("../../core/parse-exports.js");
const { useServerRegex } = require("../../constants.js");
module.exports = function (source) {
let hasUseServer = false;
if (useServerRegex.test(source)) {
hasUseServer = true;
}
if (!hasUseServer) return source;
const exports = parseExports(source);
if (exports.length === 0) return source;
// Build IDs
const moduleId = this.resourcePath;
const relativePath = path.relative(
normalizePathCase(process.cwd()),
normalizePathCase(moduleId)
);
const normalizedPath = relativePath.replace(/\\/g, "/");
const fileUrl = `file:///${normalizedPath}`;
//
// IMPORTANT: dynamic import instead of static import
//
// Webpack will NOT try to resolve "__SERVER_FUNCTION_PROXY__"
// as a module → it will remain a string → replaced later → browser loads it.
//
let proxyCode = `
const loadProxy = new Function('return import("/"+"__SERVER_FUNCTION_PROXY__")');
`;
for (const exp of exports) {
const key = exp === "default" ? `${fileUrl}#default` : `${fileUrl}#${exp}`;
if (exp === "default") {
proxyCode += `
export default (...args) =>
loadProxy().then(mod =>
(mod.default ?? mod ?? window.__SERVER_FUNCTION_PROXY_LIB__).createServerFunctionProxy(${JSON.stringify(
key,
)})(...args)
);
`;
} else {
proxyCode += `
export const ${exp} = (...args) =>
loadProxy().then(mod => (mod.default ?? mod ?? window.__SERVER_FUNCTION_PROXY_LIB__).createServerFunctionProxy(${JSON.stringify(
key,
)})(...args)
);
`;
}
}
// Emit manifest entry
const manifestEntry = {
path: normalizedPath,
exports: exports,
};
this.emitFile(
`server-functions/${normalizedPath}.json`,
JSON.stringify(manifestEntry, null, 2),
);
return proxyCode;
};⚙️ server-functions-plugin.js Walkthrough
Below is the full code of the Webpack plugin that aggregates whitelisted functions and cleans temporary assets:
const manifestGeneratorPlugin = require("./manifest-generator-plugin");
class WebpackServerFunctionsPluginSimple {
constructor() {
this.serverFunctions = new Map();
}
apply(compiler) {
const { webpack } = compiler;
const { Compilation, sources } = webpack;
// Collect files generated by the loader
compiler.hooks.thisCompilation.tap(
"WebpackServerFunctionsPluginSimple",
(compilation) => {
compilation.hooks.processAssets.tap(
{
name: "WebpackServerFunctionsPluginSimple",
stage: Compilation.PROCESS_ASSETS_STAGE_REPORT,
},
(assets) => {
// 1. Replace placeholder
const manifest = manifestGeneratorPlugin.manifestData;
const hashedPath = manifest["serverFunctionProxy.js"] || "serverFunctionProxy.js";
for (const [filename, asset] of Object.entries(assets)) {
if (filename.endsWith(".js")) {
let code = asset.source();
if (code.includes("__SERVER_FUNCTION_PROXY__")) {
code = code.replace(/__SERVER_FUNCTION_PROXY__/g, hashedPath);
compilation.updateAsset(
filename,
new sources.RawSource(code)
);
}
}
}
// 2. Collect all server function files
const serverFunctionsManifest = {};
for (const [filename, asset] of Object.entries(assets)) {
if (
filename.startsWith("server-functions/") &&
filename.endsWith(".json")
) {
try {
const content = asset.source();
const entry = JSON.parse(content);
serverFunctionsManifest[entry.path] = entry.exports;
// Delete this temporary file
delete assets[filename];
} catch (e) {
// Ignore parsing errors
}
}
}
// 3. Generate final manifest
const manifestContent = JSON.stringify(
serverFunctionsManifest,
null,
2
);
compilation.emitAsset(
"server-functions-manifest.json",
new sources.RawSource(manifestContent)
);
}
);
}
);
}
}
module.exports = WebpackServerFunctionsPluginSimple;