UNPKG

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
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 };