Styles & Assets Loading Hooks
Analyze the inner mechanics of Dinou's CommonJS require hooks for on-the-fly CSS Modules compilation with PostCSS and binary static asset loaders.
Key Files Involved:
• CSS Modules hook:./dinou/core/css-require-hook.js
• Asset extensions list:./dinou/core/asset-extensions.js
• Hashed assets hook:./dinou/core/asset-require-hook.js
💡 Overview
In standard Node.js applications, attempting to require("./styles.css") or require("./image.png") throws a runtime exception because Node's compiler only expects valid JavaScript source files.
To enable importing stylesheets and media assets directly inside React Server Components and server-side layouts, Dinou installs **custom require extension hooks** at startup. These hooks preprocess imported style files and media paths synchronously, mimicking browser bundler behaviors at the Node process level.
🎨 1. CSS Modules Hook (css-require-hook.js)
The css-require-hook.js file defines the loader for stylesheets. When registered, it overrides Node's default extension resolver for .css targets:
function registerCSSRequireHook() {
require.extensions[".css"] = function (module, filename) {
const cssContent = fs.readFileSync(filename, "utf8");
const jsonResult = {};
// 1. Process stylesheet content via PostCSS rules
postcss([plugin]).process(cssContent, { from: filename }).css;
// 2. Export the hashed classnames dictionary as a CJS module exports object
module.exports = jsonResult;
};
}This hook intercepts the file import, reads the raw CSS string, processes it, and returns a key-value mapping object (e.g. { container: "scoped_container_xyz" }).
⚙️ 2. PostCSS Selector Parser
To extract and scope class names, the require hook runs a custom, synchronous **PostCSS plugin**:
const plugin = {
postcssPlugin: "extract-classes",
Rule(rule) {
// Ignore keyframes animation tags
if (rule.parent && rule.parent.name === "keyframes") return;
const selector = rule.selector;
const classRegex = /.([_a-zA-Z0-9-]+)/g; // Match selector class tokens
let match;
while ((match = classRegex.exec(selector)) !== null) {
const className = match[1];
// Calculate scopes (:global vs :local) and register names deterministically
if (!jsonResult[className]) {
jsonResult[className] = createScopedName(className, filename);
}
}
}
};The compiler runs the plugin synchronously. The class names are fed into createScopedName.js, which returns a hashed string based on the class name and the file's path, guaranteeing that selectors don't leak across modules.
🔒 3. Hashing Scopes (:global & :local)
Sometimes you need to declare global stylesheet rules that must bypass module scoping. Dinou supports scoping rules natively:
- Local Scopes (Default): Every class selector is hashed by default (e.g.
.titlebecomes.title_a1b2c). - Global Scopes (
:global): Class names wrapped in:global(...)or declared below a:globalblock preserve their original names:/* Scoped CSS Module */ .container { padding: 20px; /* Scoped class */ } :global(.btn-active) { background-color: blue; /* Unscoped class */ }
The PostCSS parser checks if a matched selector is prefixed by :global:
const lastGlobal = before.lastIndexOf(":global");
const lastLocal = before.lastIndexOf(":local");
if (lastGlobal > lastLocal) {
// If global keyword overrides local scope, skip hashing
jsonResult[className] = className;
continue;
}🖼️ 4. Media Asset Hook (asset-require-hook.js)
Dinou registers require extensions for static media formats defined in asset-extensions.js (such as .png, .jpg, .svg, .gif, .woff2):
function hook(extension, compile) {
require.extensions[extension] = function (module, file) {
try {
const url = compile(file); // Generates static public url with hashed content
module._compile("module.exports = " + JSON.stringify(url), file);
} catch (err) {
throw err;
}
};
}🔗 5. Hashed URL Interpolation
To generate cache-friendly assets in production, Dinou uses webpack's loader-utils library to calculate deterministic hash names:
var interpolateName = require("loader-utils").interpolateName;
var result = interpolateName(context, resolvedName, {
content: content, // File binary buffer
regExp: options.regExp,
});
if (options.publicPath) {
result = options.publicPath + result; // Returns e.g. "/assets/logo.a1b2c3d4.png"
}When you import an image inside a component, the require hook copies the asset to the public assets build folder and returns the static asset url string ("/assets/[name].[hash].[ext]").
🛠️ Common Tweak Recipes
You can support SASS Modules by modifying core/css-require-hook.js. Install sass, register require.extensions[".scss"], compile the scss code to CSS first, and pass the resulting string to the PostCSS compiler.
You can change the public directory structure or naming hashes by modifying the publicPath configuration or the hash layout inside core/server.js and the corresponding loaders.