UNPKG

rollup-plugin-lib-style

Version:

A Rollup plugin that converts CSS and extensions for CSS into CSS modules and imports the generated CSS files

216 lines (177 loc) 7.03 kB
import { createFilter } from 'rollup-pluginutils'; import postcss from 'postcss'; import postcssModules from 'postcss-modules'; import crypto from 'node:crypto'; import fs from 'fs-extra'; import sass from 'sass'; import glob from 'glob'; const hashFormats = ["latin1", "hex", "base64"]; const replaceFormat = (formatString, fileName, cssContent) => { const hashLengthMatch = formatString.match(/hash:.*:(\d+)/); const hashFormatMatch = formatString.match(/hash:([^:]*)[:-]?/); const hashFormat = hashFormatMatch && hashFormats.includes(hashFormatMatch[1]) ? hashFormatMatch[1] : "hex"; const hashLength = hashLengthMatch ? parseInt(hashLengthMatch[1]) : 6; const hashString = crypto.createHash("md5").update(cssContent).digest(hashFormat); const hashToUse = hashString.length < hashLength ? hashString : hashString.slice(0, hashLength); return formatString.replace("[local]", fileName).replace(/\[hash:(.*?)(:\d+)?\]/, hashToUse) }; /** * Ensures generated class names are valid CSS identifiers. * - Replaces invalid characters with `_` * - Ensures class names do not start with a number * @param {string} name - Original class name * @returns {string} - Valid CSS class name */ const normalizeClassName = (hash) => { // Replace invalid characters with '_' let sanitized = hash.replace(/[^a-zA-Z0-9-_]/g, "_"); if (/^[0-9]/.test(sanitized)) sanitized = `_${sanitized}`; return sanitized }; const DEFAULT_SCOPED_NAME = "[local]_[hash:hex:6]"; /** * @typedef {object} postCssLoaderOptions * @property {object[]} postCssPlugins * @property {string} classNamePrefix * @property {string} scopedName */ /** * @typedef {object} postCssLoaderProps * @property {postCssLoaderOptions} options * @property {string} fiePath * @property {string} code */ /** * Transform CSS into CSS-modules * @param {postCssLoaderProps} * @returns */ const postCssLoader = async ({code, fiePath, options}) => { const {scopedName = DEFAULT_SCOPED_NAME, postCssPlugins = [], classNamePrefix = ""} = options; const modulesExported = {}; const isGlobalStyle = /\.global\.(css|scss|sass|less|stylus)$/.test(fiePath); const isInNodeModules = /[\\/]node_modules[\\/]/.test(fiePath); const postCssPluginsWithCssModules = [ postcssModules({ generateScopedName: (name, filename, css) => { const hashContent = `${filename}:${name}:${css}`; const rawScopedName = replaceFormat(scopedName, name, hashContent); const normalizedName = normalizeClassName(rawScopedName); return isInNodeModules || isGlobalStyle ? name // Use the original name for global or node_modules styles : classNamePrefix + normalizedName // Apply prefix and normalize }, getJSON: (cssFileName, json) => (modulesExported[cssFileName] = json), }), ...postCssPlugins, ]; const postcssOptions = { from: fiePath, to: fiePath, map: false, }; const result = await postcss(postCssPluginsWithCssModules).process(code, postcssOptions); // collect dependencies const dependencies = []; for (const message of result.messages) { if (message.type === "dependency") { dependencies.push(message.file); } } // print postcss warnings for (const warning of result.warnings()) { console.warn(`WARNING: ${warning.plugin}:`, warning.text); } return { code: `export default ${JSON.stringify(modulesExported[fiePath])};`, dependencies, extracted: { id: fiePath, code: result.css, }, } }; const PLUGIN_NAME = "rollup-plugin-lib-style"; const MAGIC_PATH_REGEX = /@@_MAGIC_PATH_@@/g; const MAGIC_PATH = "@@_MAGIC_PATH_@@"; const modulesIds = new Set(); const outputPaths = []; const defaultLoaders = [ { name: "sass", regex: /\.(sass|scss)$/, process: ({filePath, options}) => ({ code: sass.compile(filePath, options?.sassOptions || {}).css.toString(), }), }, { name: "css", regex: /\.(css)$/, process: ({code}) => ({code}), }, ]; const replaceMagicPath = (fileContent, customPath = ".") => fileContent.replace(MAGIC_PATH_REGEX, customPath); const libStylePlugin = (options = {}) => { const {customPath, customCSSPath, customCSSInjectedPath, loaders, include, exclude, importCSS = true, sassOptions = {}, ...postCssOptions} = options; const allLoaders = [...(loaders || []), ...defaultLoaders]; const filter = createFilter(include, exclude); const getLoader = (filepath) => allLoaders.find((loader) => loader.regex.test(filepath)); return { name: PLUGIN_NAME, options(options) { if (!options.output) console.error("missing output options"); else options.output.forEach((outputOptions) => outputPaths.push(outputOptions.dir)); }, async transform(code, id) { const loader = getLoader(id); if (!filter(id) || !loader) return null modulesIds.add(id); const rawCss = await loader.process({filePath: id, code, options: {sassOptions}}); const postCssResult = await postCssLoader({code: rawCss.code, fiePath: id, options: postCssOptions}); for (const dependency of postCssResult.dependencies) this.addWatchFile(dependency); const getFilePath = () => { return id.replace(process.cwd(), "").replace(/\\/g, "/") }; const cssFilePath = customCSSPath ? customCSSPath(id) : getFilePath(); const cssFileInjectedPath = customCSSInjectedPath ? customCSSInjectedPath(cssFilePath) : cssFilePath; const cssFilePathWithoutSlash = cssFilePath.startsWith("/") ? cssFilePath.substring(1) : cssFilePath; // create a new css file with the generated hash class names this.emitFile({ type: "asset", fileName: cssFilePathWithoutSlash.replace(loader.regex, ".css"), source: postCssResult.extracted.code, }); const importStr = importCSS ? `import "${MAGIC_PATH}${cssFileInjectedPath.replace(loader.regex, ".css")}";\n` : ""; // create a new js file with css module return { code: importStr + postCssResult.code, map: {mappings: ""}, } }, async closeBundle() { if (!importCSS) return // get all the modules that import CSS files const importersPaths = outputPaths .reduce((result, currentPath) => { result.push(glob.sync(`${currentPath}/**/*.js`)); return result }, []) .flat(); // replace magic path with relative path await Promise.all( importersPaths.map((currentPath) => fs .readFile(currentPath) .then((buffer) => buffer.toString()) .then((fileContent) => replaceMagicPath(fileContent, customPath)) .then((fileContent) => fs.writeFile(currentPath, fileContent)) ) ); }, } }; const onwarn = (warning, warn) => { if (warning.code === "UNRESOLVED_IMPORT" && warning.message.includes(MAGIC_PATH)) return if (typeof warn === "function") warn(warning); }; export { libStylePlugin, onwarn };