Style & Asset Plugins
Examine the asset loader, custom scoping classifiers, PostCSS processors, and global stylesheet extractors.
Key Files Location:
• Asset Manager:./dinou/esbuild/plugins-esbuild/assets-plugin.mjs
• CSS Processor:./dinou/esbuild/plugins-esbuild/css-processor-plugin.mjs
• Style Extractor:./dinou/esbuild/plugins-postcss/postcss-extract-plugin.js
💡 Overview
Handling stylesheets and image files in a custom bundler environment requires extra steps. When components import stylesheets (like import "./theme.css") or reference assets (like import logo from "./logo.png"), esbuild must route, extract, and rewrite paths so files can be resolved by browser requests.
📊 Assets Plugin Flow
The flowchart below shows how static files are intercepted, scoped, and resolved from JavaScript chunks:
📊 CSS Processor Flow
The flowchart below shows how CSS Modules and tailwind styles are parsed and compiled into `styles.css`:
⚙️ assets-plugin.mjs
Below is the full, complete code of the esbuild assets extraction plugin:
import fs from "node:fs/promises";
import path from "node:path";
import createScopedName from "../../core/createScopedName.js";
import { regex } from "../../core/asset-extensions.js";
import { getAbsPathWithExt } from "../../core/get-abs-path-with-ext.js";
import { pathToFileURL } from "node:url";
const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
export default function assetsPlugin({ include = regex } = {}) {
return {
name: "assets-plugin",
setup(build) {
const outdir = build.initialOptions.outdir;
if (!outdir) {
throw new Error("assetsPlugin requires outdir to be set");
}
build.initialOptions.assetNames = "assets/[name]-[hash]";
// 1. Intercept asset references
build.onResolve({ filter: include }, (args) => {
const resolvedAlias =
args.kind === "entry-point"
? args.path
: getAbsPathWithExt(args.path, {
parentURL: pathToFileURL(args.importer).href,
});
if (args.kind === "entry-point") {
return { path: resolvedAlias, namespace: "dinou-asset-entry" };
}
return { path: resolvedAlias, namespace: "dinou-asset" };
});
// 2. Read asset buffers and load them
build.onLoad({ filter: /.*/, namespace: "dinou-asset" }, async (args) => {
const contents = await fs.readFile(args.path);
return { contents, loader: "file" };
});
build.onLoad({ filter: /.*/, namespace: "dinou-asset-entry" }, async (args) => {
const contents = await fs.readFile(args.path);
return { contents, loader: "file" };
});
// 3. Process outputs, rename hashed files, and extract inlined assets from JS chunks
build.onEnd(async (result) => {
if (!result.metafile || !result.outputFiles?.length) return;
const renames = new Map();
const normalizeRel = (p) => p.replace(/\\/g, "/");
const processedSourceFiles = new Set();
// Pass 1: Normal static assets (images, icons)
for (const [oldRelPath, info] of Object.entries(result.metafile.outputs)) {
if (info.entryPoint || Object.keys(info.inputs).length !== 1) continue;
const inputPath = Object.keys(info.inputs)[0];
const sourceFile = inputPath.replace(/^dinou-asset:/, "").replace(/^dinou-asset-entry:/, "");
if (!include.test(sourceFile)) continue;
const ext = path.extname(sourceFile);
if (!oldRelPath.endsWith(ext)) continue;
const base = path.basename(sourceFile, ext);
const scoped = createScopedName(base, sourceFile);
const newLocal = `assets/${scoped}${ext}`;
const oldLocal = normalizeRel(path.relative(outdir, oldRelPath));
renames.set(oldLocal, newLocal);
processedSourceFiles.add(sourceFile);
}
// Pass 2: Extract inlined assets from within JS files
for (const [outputPath, info] of Object.entries(result.metafile.outputs)) {
if (!outputPath.endsWith(".js") || info.entryPoint) continue;
for (const inputPath of Object.keys(info.inputs)) {
if (!inputPath.startsWith("dinou-asset:")) continue;
const sourceFile = inputPath.replace(/^dinou-asset:/, "");
if (!include.test(sourceFile) || processedSourceFiles.has(sourceFile)) continue;
try {
const ext = path.extname(sourceFile);
const base = path.basename(sourceFile, ext);
const scoped = createScopedName(base, sourceFile);
const newLocal = `assets/${scoped}${ext}`;
const assetContent = await fs.readFile(sourceFile);
const newOutputFile = {
path: path.join(outdir, newLocal),
contents: assetContent,
get text() { return new TextDecoder().decode(this.contents); }
};
result.outputFiles.push(newOutputFile);
processedSourceFiles.add(sourceFile);
const chunkFile = result.outputFiles.find(
(f) => normalizeRel(path.relative(process.cwd(), f.path)) === outputPath
);
if (chunkFile) {
let chunkContent = new TextDecoder().decode(chunkFile.contents);
const assetComment = `// ${inputPath}`;
const commentIndex = chunkContent.indexOf(assetComment);
if (commentIndex !== -1) {
const nextLineStart = chunkContent.indexOf("\n", commentIndex) + 1;
const nextLineEnd = chunkContent.indexOf("\n", nextLineStart);
const assignmentLine = chunkContent.substring(nextLineStart, nextLineEnd);
const varMatch = assignmentLine.match(/var (\w+)_default = "([^"]+)"/);
if (varMatch) {
const varName = varMatch[1];
const newAssignmentLine = `var ${varName}_default = "/${newLocal}";`;
chunkContent = chunkContent.substring(0, nextLineStart) + newAssignmentLine + chunkContent.substring(nextLineEnd);
}
}
chunkFile.contents = new TextEncoder().encode(chunkContent);
}
} catch (error) {
console.error(`Error extracting asset ${sourceFile} from chunk:`, error);
}
}
}
// Pass 3: Rewrite paths across JS/CSS output files
for (const file of result.outputFiles) {
const relPath = normalizeRel(path.relative(process.cwd(), file.path));
if (!relPath.endsWith(".js") && !relPath.endsWith(".css")) continue;
let content = new TextDecoder().decode(file.contents);
for (const [oldLocal, newLocal] of renames) {
const patterns = [
[`"./${escapeRegExp(oldLocal)}"`, `"/${newLocal}"`],
[`"${escapeRegExp(oldLocal)}"`, `"${newLocal}"`],
];
for (const [oldPattern, newPattern] of patterns) {
content = content.replace(new RegExp(oldPattern, "g"), newPattern);
}
}
file.contents = new TextEncoder().encode(content);
}
// Apply final paths
for (const file of result.outputFiles) {
const relPath = normalizeRel(path.relative(process.cwd(), file.path));
const oldLocal = normalizeRel(path.relative(outdir, relPath));
const newLocal = renames.get(oldLocal);
if (newLocal) {
file.path = path.join(outdir, newLocal);
}
}
});
},
};
}⚙️ css-processor-plugin.mjs
Below is the full, complete code of the PostCSS compilation plugin:
import fs from "node:fs/promises";
import path from "node:path";
import tailwindcss from "@tailwindcss/postcss";
import autoprefixer from "autoprefixer";
import createScopedName from "../../core/createScopedName.js";
import postCssModules from "postcss-modules";
import postcss from "postcss";
import postcssImport from "postcss-import";
import { getAbsPathWithExt } from "../../core/get-abs-path-with-ext.js";
import { pathToFileURL } from "node:url";
import resolve from "resolve";
import createPostCSSExtractPlugin from "../plugins-postcss/postcss-extract-plugin.js";
export default function cssProcessorPlugin({ outdir = "public" } = {}) {
const { finalize, plugin: extractor } = createPostCSSExtractPlugin({
outputFile: `${outdir}/styles.css`,
});
return {
name: "css-processor",
setup(build) {
build.onLoad({ filter: /\.css$/ }, async (args) => {
const filePath = args.path;
const source = await fs.readFile(filePath, "utf8");
let map = {};
// Process CSS using PostCSS chain
await postcss([
postcssImport({
resolve: (id, basedir) => {
const resolvedAlias = getAbsPathWithExt(id, {
parentURL: pathToFileURL(basedir).href,
});
if (resolvedAlias) return resolvedAlias;
if (id.startsWith("tailwindcss/")) {
return resolve.sync(id, { basedir, extensions: [".css"] });
}
return resolve.sync(id, { basedir, extensions: [".css"] });
},
}),
tailwindcss(),
autoprefixer,
postCssModules({
generateScopedName: (name, filename) => {
if (!filename.endsWith(".module.css")) return name;
return createScopedName(name, filename);
},
getJSON: (_, json) => { map = json; },
}),
extractor, // Extract rules and strip duplicate injections
]).process(source, { from: filePath });
// If it is a CSS module, return class mapper exports to javascript
if (filePath.endsWith(".module.css")) {
return {
contents: `export default ${JSON.stringify(map)};`,
loader: "js",
};
} else {
return {
contents: `/* global: ${path.basename(filePath)} */`,
loader: "js",
};
}
});
build.onEnd(() => {
finalize();
});
},
};
}⚙️ postcss-extract-plugin.js
Below is the full, complete code of the PostCSS stylesheet extractor:
const fs = require("fs");
const path = require("path");
const createPostCSSExtractPlugin = (options = {}) => {
const { outputFile = "styles.css", shouldExtract = () => true } = options;
let extractedCSS = "";
const postcssPlugin = {
postcssPlugin: "postcss-extract",
OnceExit(root, { result }) {
const filePath = result.opts.from;
if (shouldExtract(filePath, root)) {
extractedCSS += root.toString();
extractedCSS += "\n";
// Remove CSS rules from raw file to prevent duplicate injections in JS
root.removeAll();
}
},
};
const finalize = () => {
if (!extractedCSS) return;
const outputDir = path.dirname(outputFile);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
fs.writeFileSync(outputFile, extractedCSS);
extractedCSS = "";
};
return {
plugin: postcssPlugin,
finalize,
};
};
module.exports = createPostCSSExtractPlugin;