UNPKG

esbuild-css-modules-plugin

Version:

A esbuild plugin to bundle css modules into js(x)/ts(x), based on extremely fast [Lightning CSS](https://lightningcss.dev/)

252 lines (230 loc) 6.13 kB
import { isAbsolute, resolve, sep, relative, basename, dirname } from 'node:path'; import { sep as posixSep } from 'node:path/posix' import { createHash } from 'node:crypto'; import { accessSync, constants } from 'node:fs'; import { createRequire } from 'node:module'; import { omit } from 'lodash-es'; import { writeFile, mkdir } from 'node:fs/promises'; const require = createRequire(import.meta.url); const pluginName = `esbuild-css-modules-plugin`; const pluginJsNamespace = `${pluginName}-ns-js`; const pluginCssNamespace = `${pluginName}-ns-css`; const pluginInjectMethodName = `__${pluginName.replaceAll('-', '_')}_inject__`; const nsPathRegexp = new RegExp(`^${pluginJsNamespace}:`, 'i'); /** * getModulesCssRegExp * @param {import('../index.js').Options} options * @returns {RegExp} */ const getModulesCssRegExp = (options) => { return options.filter ?? /\.modules?\.css$/i; }; /** * getLogger * @param {import('esbuild').PluginBuild} build * @returns {(...args: any[]) => void} */ const getLogger = (build) => { const { logLevel } = build.initialOptions; if (logLevel === 'debug' || logLevel === 'verbose') { return (...args) => { console.log(`[${pluginName}]`, ...args); }; } return () => undefined; }; /** * getRootDir * @param {import('esbuild').PluginBuild} build * @returns {string} */ const getRootDir = (build) => { const { absWorkingDir } = build.initialOptions; const abs = absWorkingDir ? absWorkingDir : process.cwd(); const rootDir = isAbsolute(abs) ? resolve(abs) : resolve(process.cwd(), abs); return rootDir; }; /** * getPackageInfo * @param {import('esbuild').PluginBuild} build * @param {import('../index.js').Options} [options] * @returns {{name: string; version: string;}} */ const getPackageInfo = (build, options) => { const rootDir = getRootDir(build); if (options?.package?.name) { return { name: options.package.name, version: options.package.version }; } const packageJsonFile = resolve(rootDir, './package.json'); try { accessSync(packageJsonFile, constants.R_OK); return require(packageJsonFile); } catch (error) { return { name: '', version: '' }; } }; /** * getRelativePath * @description get relative path (starts with `.`) from build root * @param {import('../index.js').Build} build * @param {string} to * @returns {string} */ const getRelativePath = (build, to) => { if (!isAbsolute(to)) { return to.startsWith('.') ? to : `.${sep}${to}`; } const root = build.context?.buildRoot ?? getRootDir(build); return `.${sep}${relative(root, to)}`; }; /** * getBuildId * @description buildId should be stable so that the hash of output files are stable * @param {import('esbuild').PluginBuild} build * @returns {string} */ const getBuildId = (build) => { const { entryPoints, stdin } = build.initialOptions; const buildRoot = getRootDir(build); const { version: packageVersion, name: packageName } = getPackageInfo(build); const identity = JSON.stringify({ ...omit(build.initialOptions, ['outdir', 'outfile', 'nodePaths', 'absWorkingDir', 'plugins']), base: basename(buildRoot), entryPoints: entryPoints ?? '', stdin: stdin ?? '', packageName, packageVersion }); return createHash('md5').update(identity).digest('hex'); }; const jsKeywords = [ 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'implements', 'import', 'in', 'instanceof', 'interface', 'let', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'super', 'switch', 'static', 'this', 'throw', 'try', 'true', 'typeof', 'var', 'void', 'while', 'with', 'yield' ]; /** * @param {string} name * @returns {boolean} */ const validateNamedExport = (name) => { return !jsKeywords.includes(name); }; const contentPlaceholder = '__content_placeholder__'; const digestPlaceholder = '__digest_placeholder__'; /** * @param {string} to * @returns {string} */ const relativeToCwd = (to) => relative(process.cwd(), to); const genDigest = (/** @type {string} */ str, /** @type {string} */ buildId) => createHash('md5').update(`${pluginName}:${buildId}:${str}`).digest('hex'); const validateOptions = (/** @type {import('../index.js').Options} */ options) => { if (options.inject && options.namedExports) { throw new Error(`[${pluginName}]: \`inject\` & \`namedExports\` can not be used at same time`); } if (options.forceInlineImages) { if (options.force === false) { // yellow: enabled ? formatter("\x1b[33m", "\x1b[39m") : String, console.log( `\x1b[33m[${pluginName}] Warning: \`force\` will always be \`true\` when \`forceInlineImages\` is \`true\`\x1b[39m` ); } options.force = true; } }; const injectorVirtualPath = 'injector.js'; const simpleMinifyCss = ( /** @type {string} */ css, /** @type {import('esbuild').PluginBuild['esbuild']} */ esbuild ) => esbuild ? esbuild.transformSync(css, { loader: 'css', minify: true, sourcemap: false }).code : css.replaceAll(/(\\n|\n|\/\*.+?\*\/)/g, '').replaceAll(/\s+/g, ' '); /** * ensureFile * @param {string} filepath * @param {string} data */ const ensureFile = async (filepath, data = '') => { if (!filepath) { return; } const dir = dirname(filepath); await mkdir(dir, { recursive: true }); await writeFile(filepath, `${data}`.trim(), { encoding: 'utf8' }); }; export { pluginName, pluginJsNamespace, pluginCssNamespace, nsPathRegexp, getLogger, getRootDir, getModulesCssRegExp, getRelativePath, getBuildId, validateNamedExport, getPackageInfo, contentPlaceholder, digestPlaceholder, relativeToCwd, genDigest, validateOptions, injectorVirtualPath, pluginInjectMethodName, simpleMinifyCss, ensureFile }; /** * @param {string} p import path * @return {string} */ export const fixImportPath = (p) => { return p.split(sep).join(posixSep); }