esbuild-style-loader
Version:
A style loader for esbuild, support for CSS, SCSS, LESS, Stylus, and CSS Modules.
500 lines (492 loc) • 16.1 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/index.mts
import { readFile as readFile3 } from "node:fs/promises";
import PATH2 from "node:path";
import colors from "colors";
import deepmerge from "deepmerge";
import { transform } from "lightningcss";
import qs from "query-string";
// src/less-utils.mts
var convertLessError = (error) => {
const sourceLine = error.extract.filter(Boolean);
const lineText = sourceLine.length === 3 ? sourceLine[1] : sourceLine[0];
return {
text: error.message || "less compile error",
location: {
namespace: "file",
file: error.filename,
line: error.line,
column: error.column,
lineText
}
};
};
// src/sass-utils.mts
import path from "node:path";
function getDefaultSassImplementation() {
let sassImplPkg = "sass";
try {
__require.resolve("sass");
} catch (ignoreError) {
try {
__require.resolve("node-sass");
sassImplPkg = "node-sass";
} catch (_ignoreError) {
try {
__require.resolve("sass-embedded");
sassImplPkg = "sass-embedded";
} catch (__ignoreError) {
sassImplPkg = "sass";
}
}
}
return __require(sassImplPkg);
}
function fileSyntax(filename) {
if (filename.endsWith(".scss")) {
return "scss";
}
return "indented";
}
function resolveCanonicalize(importer, file, alias) {
const importerExt = path.extname(importer);
const fileExt = path.extname(file);
if (!fileExt) {
file += importerExt;
}
if (path.isAbsolute(file)) {
return file;
}
const scope = file.split("/")[0];
if (alias[scope]) {
const baseFile = file.slice(scope.length + 1);
return path.resolve(alias[scope], baseFile);
}
return path.resolve(path.dirname(importer), file);
}
function convertScssError(error, filePath) {
const [message, _, lineText, ...rest] = error.message.split("\n");
const stack = rest[rest.length - 1];
const lineColumn = stack.match(/(\d+):(\d+)/);
return {
text: message,
location: {
file: filePath,
line: Number(lineColumn[1]),
column: Number(lineColumn[2]),
lineText: lineText.trim().replace(/\d+\s│/, "")
}
};
}
// src/transform-less.mts
import { readFile } from "node:fs/promises";
import lessEngine from "less";
import { LessPluginModuleResolver } from "less-plugin-module-resolver";
var transformLess = (filePath, options) => __async(void 0, null, function* () {
const code = yield readFile(filePath, "utf-8");
const result = yield lessEngine.render(code, {
filename: filePath,
syncImport: true,
/**
* Legacy compatible
*/
plugins: [
new LessPluginModuleResolver({
alias: Object.assign({ "~": "" }, options.alias)
})
],
sourceMap: options.sourcemap ? {
sourceMapFileInline: false,
outputSourceFiles: true
} : void 0
});
let { css, map, imports } = result;
imports = imports.filter((item) => item !== filePath);
return { css, map, imports };
});
// src/transform-sass.mts
import { readFile as readFile2 } from "node:fs/promises";
import * as fs from "node:fs";
import path2 from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
var sassEngine;
var transformSass = (filePath, options) => __async(void 0, null, function* () {
const { sourcemap } = options;
if (!sassEngine) {
try {
sassEngine = getDefaultSassImplementation();
} catch (e) {
console.error(e);
process.exit(1);
}
}
const syntax = fileSyntax(filePath);
const basedir = path2.dirname(filePath);
const contents = yield readFile2(filePath, "utf-8");
const warnings = [];
const alias = options.alias || {};
const result = yield sassEngine.compileStringAsync(contents, {
sourceMapIncludeSources: true,
sourceMap: sourcemap,
syntax,
alertColor: false,
logger: {
warn(message, options2) {
var _a, _b;
if (!options2.span) {
warnings.push({ text: `sass warning: ${message}` });
} else {
const filename = (_b = (_a = options2.span.url) == null ? void 0 : _a.pathname) != null ? _b : filePath;
const esbuildMsg = {
text: message,
location: {
file: filename,
line: options2.span.start.line,
column: options2.span.start.column,
lineText: options2.span.text
},
detail: {
deprecation: options2.deprecation,
stack: options2.stack
}
};
warnings.push(esbuildMsg);
}
}
},
importer: {
load(canonicalUrl) {
const pathname = fileURLToPath(canonicalUrl);
const contents2 = fs.readFileSync(pathname, "utf8");
return {
contents: contents2,
syntax: fileSyntax(pathname),
sourceMapUrl: sourcemap ? canonicalUrl : void 0
};
},
canonicalize(url) {
const urlPath = resolveCanonicalize(filePath, url, alias);
if (urlPath) {
return pathToFileURL(urlPath);
}
return null;
}
}
});
const sourceMap = result.sourceMap;
if (sourceMap) {
sourceMap.sourceRoot = basedir;
sourceMap.sources = sourceMap.sources.map((source) => {
return path2.relative(basedir, source.startsWith("data:") ? filePath : fileURLToPath(source));
});
}
const imports = result.loadedUrls.map((url) => fileURLToPath(url));
return {
css: result.css,
map: result.sourceMap ? JSON.stringify(result.sourceMap) : "",
imports,
warnings
};
});
// src/utils.mts
import PATH from "node:path";
import browserslist from "browserslist";
import { camelCase } from "change-case";
import { browserslistToTargets } from "lightningcss";
var codeWithSourceMap = (code, map) => {
return `${code}/*# sourceMappingURL=data:application/json;base64,${Buffer.from(map).toString("base64")} */`;
};
var cssExportsToJs = (exports, entryFile) => {
const keys = Object.keys(exports).sort();
const values = keys.map((key) => exports[key]);
let variablesCode = "";
const exportCode = [];
const defaultCode = [];
keys.forEach((key, index) => {
const clsVar = `s_${camelCase(key)}`;
const clsSelector = values[index].name;
variablesCode += `var ${clsVar} = "${clsSelector}";
`;
exportCode.push(`exports['${key}'] = ${clsVar};`);
defaultCode.push(`'${key}':${clsVar}`);
});
const code = `${variablesCode};
${exportCode.join("\n")};
module.exports = {${defaultCode.join(",")}};
`;
if (entryFile) {
return `require('${entryFile}');
${code}`;
}
return code;
};
var parsePath = (path3) => {
const queryIndex = path3.indexOf("?");
if (queryIndex === -1) {
return { path: path3, query: "" };
}
return {
path: path3.slice(0, queryIndex),
query: path3.slice(queryIndex + 1)
};
};
function resolvePath(args, build) {
return __async(this, null, function* () {
const { path: path3, query } = parsePath(args.path);
let absolutePath = path3;
if (!PATH.isAbsolute(absolutePath)) {
const result = yield build.resolve(absolutePath, {
resolveDir: args.resolveDir,
importer: args.importer,
kind: args.kind
});
absolutePath = result.path;
}
return { path: absolutePath, query };
});
}
var generateTargets = (queries) => {
return browserslistToTargets(browserslist(queries));
};
var replaceExtension = (file, ext) => {
const extName = PATH.extname(file);
return file.slice(0, file.length - extName.length) + ext;
};
// src/index.mts
colors.enable();
var defaultOptions = {
filter: /\.(css|scss|sass|less)(\?.*)?$/,
cssModules: { pattern: "[local]__[hash]" },
browsers: "> 0.25%, not dead"
};
var styleLoader = (options = {}) => {
const opts = deepmerge(defaultOptions, options);
const targets = generateTargets(opts.browsers);
const allNamespaces = Array.from(new Set(["file"].concat(opts.namespace || [])));
const getLogger = (build) => {
const { logLevel } = build.initialOptions;
if (logLevel === "debug" || logLevel === "verbose") {
return (...args) => {
console.log("[esbuild-style-loader]".magenta.bold, ...args);
};
}
return () => void 0;
};
return {
name: "style-loader",
setup(build) {
const buildOptions = build.initialOptions;
const logger = getLogger(build);
const cwd = process.cwd();
const styleTransform = (filePath) => __async(this, null, function* () {
const extname = PATH2.extname(filePath);
if (extname === ".less") {
return yield transformLess(filePath, {
sourcemap: !!buildOptions.sourcemap,
alias: buildOptions.alias
}).catch((error) => {
logger(`transform less error: ${filePath}`.red.bold);
logger(error);
throw convertLessError(error);
});
}
if (extname === ".styl") {
throw new Error("stylus is not supported yet");
}
if (extname === ".scss" || extname === ".sass") {
return yield transformSass(filePath, {
sourcemap: !!buildOptions.sourcemap,
alias: buildOptions.alias
}).catch((error) => {
logger(`transform sass error: ${filePath}`.red.bold);
logger(error);
throw convertScssError(error, filePath);
});
}
const code = yield readFile3(filePath, "utf-8");
return { css: code, map: "" };
});
const handleResolve = (args) => __async(this, null, function* () {
const { path: fullPath, query } = yield resolvePath(args, build);
return {
path: fullPath,
namespace: "style-loader",
pluginData: {
rawPath: args.path,
query: qs.parse(query),
resolveDir: args.resolveDir
}
};
});
for (const namespace of allNamespaces) {
build.onResolve({ filter: opts.filter, namespace }, handleResolve);
}
build.onLoad({ filter: /.*/, namespace: "style-loader" }, (args) => __async(this, null, function* () {
let cssContent;
let cssSourceMap;
let watchImports = [];
const pluginData = args.pluginData;
let entryContent = cssExportsToJs({}, pluginData.rawPath);
const styleFile = args.path;
const enableCssModules = /\.modules?\.(css|less|sass|scss)/.test(styleFile) || "modules" in pluginData.query;
let result;
try {
const t = Date.now();
result = yield styleTransform(styleFile);
logger("Compile", PATH2.relative(cwd, styleFile).blue.underline, `in ${Date.now() - t}ms`);
cssContent = result.css;
cssSourceMap = result.map;
watchImports = result.imports;
} catch (error) {
return {
errors: [error],
resolveDir: PATH2.dirname(styleFile),
watchFiles: [styleFile]
};
}
let transformResult;
const watchFiles = [styleFile];
if (watchImports) {
watchFiles.push(...watchImports);
}
try {
const t = Date.now();
transformResult = transform({
targets,
inputSourceMap: cssSourceMap,
sourceMap: true,
filename: styleFile,
cssModules: enableCssModules ? opts.cssModules : false,
code: Buffer.from(cssContent)
});
logger(`Transform css in ${Date.now() - t}ms`);
} catch (error) {
logger(`Transform css error: ${styleFile}`.red.bold);
logger(error);
const { loc, fileName, source } = error;
const lines = source.split("\n");
const lineText = lines[loc.line - 1];
return {
errors: [
{
text: error.message,
location: {
file: replaceExtension(fileName, ".css"),
line: loc.line,
column: loc.column,
lineText
}
}
],
resolveDir: PATH2.dirname(styleFile),
watchFiles
};
}
const { code, map, exports } = transformResult;
if (buildOptions.sourcemap && map) {
cssContent = codeWithSourceMap(code.toString(), map.toString());
} else {
cssContent = code.toString();
}
if (exports) {
entryContent = cssExportsToJs(exports, pluginData.rawPath);
}
return {
contents: entryContent,
loader: "js",
pluginName: "style-loader",
resolveDir: pluginData.resolveDir,
watchFiles,
pluginData: __spreadProps(__spreadValues({}, pluginData), { cssContent }),
warnings: result.warnings
};
}));
build.onResolve({ filter: opts.filter, namespace: "style-loader" }, (args) => __async(this, null, function* () {
const parsedPath = parsePath(args.path);
const result = yield build.resolve(parsedPath.path, {
kind: args.kind,
resolveDir: args.resolveDir,
importer: args.importer
});
if (result.errors.length) {
return {
errors: result.errors,
resolveDir: PATH2.dirname(args.path),
watchFiles: [args.path]
};
}
return {
path: result.path,
namespace: "css-loader",
pluginData: args.pluginData,
watchFiles: [args.path]
};
}));
build.onLoad({ filter: /.*/, namespace: "css-loader" }, (args) => __async(this, null, function* () {
const pluginData = args.pluginData;
const { cssContent, rawPath } = pluginData;
return {
contents: cssContent,
loader: "css",
resolveDir: PATH2.dirname(args.path),
watchFiles: [rawPath]
};
}));
build.onResolve({ filter: /^\//, namespace: "css-loader" }, (args) => __async(this, null, function* () {
if (opts.publicPath) {
return { path: PATH2.join(opts.publicPath, `.${args.path}`) };
}
return { external: true };
}));
}
};
};
export {
convertLessError,
styleLoader,
transformLess
};