UNPKG

vite-plugin-style-modules

Version:

⭐️⭐️⭐️ Support CSS Modules, not only in xx.module.xx, but also in the custom named file. like xx.(css|less|sass|stylus)

465 lines (427 loc) 15.8 kB
import require$$0 from 'path'; var utils = {}; const path = require$$0; const WIN_SLASH = '\\\\/'; const WIN_NO_SLASH = `[^${WIN_SLASH}]`; /** * Posix glob regex */ const DOT_LITERAL = '\\.'; const PLUS_LITERAL = '\\+'; const QMARK_LITERAL = '\\?'; const SLASH_LITERAL = '\\/'; const ONE_CHAR = '(?=.)'; const QMARK = '[^/]'; const END_ANCHOR = `(?:${SLASH_LITERAL}|$)`; const START_ANCHOR = `(?:^|${SLASH_LITERAL})`; const DOTS_SLASH = `${DOT_LITERAL}{1,2}${END_ANCHOR}`; const NO_DOT = `(?!${DOT_LITERAL})`; const NO_DOTS = `(?!${START_ANCHOR}${DOTS_SLASH})`; const NO_DOT_SLASH = `(?!${DOT_LITERAL}{0,1}${END_ANCHOR})`; const NO_DOTS_SLASH = `(?!${DOTS_SLASH})`; const QMARK_NO_DOT = `[^.${SLASH_LITERAL}]`; const STAR = `${QMARK}*?`; const POSIX_CHARS = { DOT_LITERAL, PLUS_LITERAL, QMARK_LITERAL, SLASH_LITERAL, ONE_CHAR, QMARK, END_ANCHOR, DOTS_SLASH, NO_DOT, NO_DOTS, NO_DOT_SLASH, NO_DOTS_SLASH, QMARK_NO_DOT, STAR, START_ANCHOR }; /** * Windows glob regex */ const WINDOWS_CHARS = { ...POSIX_CHARS, SLASH_LITERAL: `[${WIN_SLASH}]`, QMARK: WIN_NO_SLASH, STAR: `${WIN_NO_SLASH}*?`, DOTS_SLASH: `${DOT_LITERAL}{1,2}(?:[${WIN_SLASH}]|$)`, NO_DOT: `(?!${DOT_LITERAL})`, NO_DOTS: `(?!(?:^|[${WIN_SLASH}])${DOT_LITERAL}{1,2}(?:[${WIN_SLASH}]|$))`, NO_DOT_SLASH: `(?!${DOT_LITERAL}{0,1}(?:[${WIN_SLASH}]|$))`, NO_DOTS_SLASH: `(?!${DOT_LITERAL}{1,2}(?:[${WIN_SLASH}]|$))`, QMARK_NO_DOT: `[^.${WIN_SLASH}]`, START_ANCHOR: `(?:^|[${WIN_SLASH}])`, END_ANCHOR: `(?:[${WIN_SLASH}]|$)` }; /** * POSIX Bracket Regex */ const POSIX_REGEX_SOURCE = { alnum: 'a-zA-Z0-9', alpha: 'a-zA-Z', ascii: '\\x00-\\x7F', blank: ' \\t', cntrl: '\\x00-\\x1F\\x7F', digit: '0-9', graph: '\\x21-\\x7E', lower: 'a-z', print: '\\x20-\\x7E ', punct: '\\-!"#$%&\'()\\*+,./:;<=>?@[\\]^_`{|}~', space: ' \\t\\r\\n\\v\\f', upper: 'A-Z', word: 'A-Za-z0-9_', xdigit: 'A-Fa-f0-9' }; var constants = { MAX_LENGTH: 1024 * 64, POSIX_REGEX_SOURCE, // regular expressions REGEX_BACKSLASH: /\\(?![*+?^${}(|)[\]])/g, REGEX_NON_SPECIAL_CHARS: /^[^@![\].,$*+?^{}()|\\/]+/, REGEX_SPECIAL_CHARS: /[-*+?.^${}(|)[\]]/, REGEX_SPECIAL_CHARS_BACKREF: /(\\?)((\W)(\3*))/g, REGEX_SPECIAL_CHARS_GLOBAL: /([-*+?.^${}(|)[\]])/g, REGEX_REMOVE_BACKSLASH: /(?:\[.*?[^\\]\]|\\(?=.))/g, // Replace globs with equivalent patterns to reduce parsing time. REPLACEMENTS: { '***': '*', '**/**': '**', '**/**/**': '**' }, // Digits CHAR_0: 48, /* 0 */ CHAR_9: 57, /* 9 */ // Alphabet chars. CHAR_UPPERCASE_A: 65, /* A */ CHAR_LOWERCASE_A: 97, /* a */ CHAR_UPPERCASE_Z: 90, /* Z */ CHAR_LOWERCASE_Z: 122, /* z */ CHAR_LEFT_PARENTHESES: 40, /* ( */ CHAR_RIGHT_PARENTHESES: 41, /* ) */ CHAR_ASTERISK: 42, /* * */ // Non-alphabetic chars. CHAR_AMPERSAND: 38, /* & */ CHAR_AT: 64, /* @ */ CHAR_BACKWARD_SLASH: 92, /* \ */ CHAR_CARRIAGE_RETURN: 13, /* \r */ CHAR_CIRCUMFLEX_ACCENT: 94, /* ^ */ CHAR_COLON: 58, /* : */ CHAR_COMMA: 44, /* , */ CHAR_DOT: 46, /* . */ CHAR_DOUBLE_QUOTE: 34, /* " */ CHAR_EQUAL: 61, /* = */ CHAR_EXCLAMATION_MARK: 33, /* ! */ CHAR_FORM_FEED: 12, /* \f */ CHAR_FORWARD_SLASH: 47, /* / */ CHAR_GRAVE_ACCENT: 96, /* ` */ CHAR_HASH: 35, /* # */ CHAR_HYPHEN_MINUS: 45, /* - */ CHAR_LEFT_ANGLE_BRACKET: 60, /* < */ CHAR_LEFT_CURLY_BRACE: 123, /* { */ CHAR_LEFT_SQUARE_BRACKET: 91, /* [ */ CHAR_LINE_FEED: 10, /* \n */ CHAR_NO_BREAK_SPACE: 160, /* \u00A0 */ CHAR_PERCENT: 37, /* % */ CHAR_PLUS: 43, /* + */ CHAR_QUESTION_MARK: 63, /* ? */ CHAR_RIGHT_ANGLE_BRACKET: 62, /* > */ CHAR_RIGHT_CURLY_BRACE: 125, /* } */ CHAR_RIGHT_SQUARE_BRACKET: 93, /* ] */ CHAR_SEMICOLON: 59, /* ; */ CHAR_SINGLE_QUOTE: 39, /* ' */ CHAR_SPACE: 32, /* */ CHAR_TAB: 9, /* \t */ CHAR_UNDERSCORE: 95, /* _ */ CHAR_VERTICAL_LINE: 124, /* | */ CHAR_ZERO_WIDTH_NOBREAK_SPACE: 65279, /* \uFEFF */ SEP: path.sep, /** * Create EXTGLOB_CHARS */ extglobChars(chars) { return { '!': { type: 'negate', open: '(?:(?!(?:', close: `))${chars.STAR})` }, '?': { type: 'qmark', open: '(?:', close: ')?' }, '+': { type: 'plus', open: '(?:', close: ')+' }, '*': { type: 'star', open: '(?:', close: ')*' }, '@': { type: 'at', open: '(?:', close: ')' } }; }, /** * Create GLOB_CHARS */ globChars(win32) { return win32 === true ? WINDOWS_CHARS : POSIX_CHARS; } }; (function (exports) { const path = require$$0; const win32 = process.platform === 'win32'; const { REGEX_BACKSLASH, REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL } = constants; exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); exports.isRegexChar = str => str.length === 1 && exports.hasRegexChars(str); exports.escapeRegex = str => str.replace(REGEX_SPECIAL_CHARS_GLOBAL, '\\$1'); exports.toPosixSlashes = str => str.replace(REGEX_BACKSLASH, '/'); exports.removeBackslashes = str => { return str.replace(REGEX_REMOVE_BACKSLASH, match => { return match === '\\' ? '' : match; }); }; exports.supportsLookbehinds = () => { const segs = process.version.slice(1).split('.').map(Number); if (segs.length === 3 && segs[0] >= 9 || (segs[0] === 8 && segs[1] >= 10)) { return true; } return false; }; exports.isWindows = options => { if (options && typeof options.windows === 'boolean') { return options.windows; } return win32 === true || path.sep === '\\'; }; exports.escapeLast = (input, char, lastIdx) => { const idx = input.lastIndexOf(char, lastIdx); if (idx === -1) return input; if (input[idx - 1] === '\\') return exports.escapeLast(input, char, idx - 1); return `${input.slice(0, idx)}\\${input.slice(idx)}`; }; exports.removePrefix = (input, state = {}) => { let output = input; if (output.startsWith('./')) { output = output.slice(2); state.prefix = './'; } return output; }; exports.wrapOutput = (input, state = {}, options = {}) => { const prepend = options.contains ? '' : '^'; const append = options.contains ? '' : '$'; let output = `${prepend}(?:${input})${append}`; if (state.negated === true) { output = `(?:^(?!${output}).*$)`; } return output; }; }(utils)); const reservedWords = 'break case class catch const continue debugger default delete do else export extends finally for function if import in instanceof let new return super switch this throw try typeof var void while with yield enum await implements package protected static interface private public'; const builtins = 'arguments Infinity NaN undefined null true false eval uneval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Symbol Error EvalError InternalError RangeError ReferenceError SyntaxError TypeError URIError Number Math Date String RegExp Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array Map Set WeakMap WeakSet SIMD ArrayBuffer DataView JSON Promise Generator GeneratorFunction Reflect Proxy Intl'; const forbiddenIdentifiers = new Set(`${reservedWords} ${builtins}`.split(' ')); forbiddenIdentifiers.add(''); const makeLegalIdentifier = function makeLegalIdentifier(str) { let identifier = str .replace(/-(\w)/g, (_, letter) => letter.toUpperCase()) .replace(/[^$_a-zA-Z0-9]/g, '_'); if (/\d/.test(identifier[0]) || forbiddenIdentifiers.has(identifier)) { identifier = `_${identifier}`; } return identifier || '_'; }; function stringify(obj) { return (JSON.stringify(obj) || 'undefined').replace(/[\u2028\u2029]/g, (char) => `\\u${`000${char.charCodeAt(0).toString(16)}`.slice(-4)}`); } function serializeArray(arr, indent, baseIndent) { let output = '['; const separator = indent ? `\n${baseIndent}${indent}` : ''; for (let i = 0; i < arr.length; i++) { const key = arr[i]; output += `${i > 0 ? ',' : ''}${separator}${serialize(key, indent, baseIndent + indent)}`; } return `${output}${indent ? `\n${baseIndent}` : ''}]`; } function serializeObject(obj, indent, baseIndent) { let output = '{'; const separator = indent ? `\n${baseIndent}${indent}` : ''; const entries = Object.entries(obj); for (let i = 0; i < entries.length; i++) { const [key, value] = entries[i]; const stringKey = makeLegalIdentifier(key) === key ? key : stringify(key); output += `${i > 0 ? ',' : ''}${separator}${stringKey}:${indent ? ' ' : ''}${serialize(value, indent, baseIndent + indent)}`; } return `${output}${indent ? `\n${baseIndent}` : ''}}`; } function serialize(obj, indent, baseIndent) { if (obj === Infinity) return 'Infinity'; if (obj === -Infinity) return '-Infinity'; if (obj === 0 && 1 / obj === -Infinity) return '-0'; if (obj instanceof Date) return `new Date(${obj.getTime()})`; if (obj instanceof RegExp) return obj.toString(); if (obj !== obj) return 'NaN'; // eslint-disable-line no-self-compare if (Array.isArray(obj)) return serializeArray(obj, indent, baseIndent); if (obj === null) return 'null'; if (typeof obj === 'object') return serializeObject(obj, indent, baseIndent); return stringify(obj); } const dataToEsm = function dataToEsm(data, options = {}) { const t = options.compact ? '' : 'indent' in options ? options.indent : '\t'; const _ = options.compact ? '' : ' '; const n = options.compact ? '' : '\n'; const declarationType = options.preferConst ? 'const' : 'var'; if (options.namedExports === false || typeof data !== 'object' || Array.isArray(data) || data instanceof Date || data instanceof RegExp || data === null) { const code = serialize(data, options.compact ? null : t, ''); const magic = _ || (/^[{[\-\/]/.test(code) ? '' : ' '); // eslint-disable-line no-useless-escape return `export default${magic}${code};`; } let namedExportCode = ''; const defaultExportRows = []; for (const [key, value] of Object.entries(data)) { if (key === makeLegalIdentifier(key)) { if (options.objectShorthand) defaultExportRows.push(key); else defaultExportRows.push(`${key}:${_}${key}`); namedExportCode += `export ${declarationType} ${key}${_}=${_}${serialize(value, options.compact ? null : t, '')};${n}`; } else { defaultExportRows.push(`${stringify(key)}:${_}${serialize(value, options.compact ? null : t, '')}`); } } return `${namedExportCode}export default${_}{${n}${t}${defaultExportRows.join(`,${n}${t}`)}${n}};${n}`; }; // css匹配规则 const cssLangs = /\.(css|less|scss|stylus|styl)/; // 模块化css匹配规则 let cssModuleLangs = /\.(css|less|scss|stylus|styl)/, // css模块化后的json结构 cssModuleJSON, // css模块化参数 modulesOptions = { scopeBehaviour: 'local', localsConvention: 'camelCase', }, postcssPlugins = [ require('postcss-nested')(), ]; async function compileCSS(id, code) { let moduleJson; const _postcssPlugins = [ ...postcssPlugins, require('postcss-modules')({ ...modulesOptions, getJSON(cssFileName, module, outputFileName) { moduleJson = module; if (modulesOptions && typeof modulesOptions.getJSON === 'function') { modulesOptions.getJSON(cssFileName, module, outputFileName); } }, }), ]; // 根据文件名获取对应的css编译器,这里本身的错误提示就很完美了,不需要人工catch const lang = id.match(cssLangs)[1]; const parser = lang !== 'css' ? require(`postcss-${lang}`) : undefined; const nextCode = await require('postcss') .default(_postcssPlugins) .process(code, { parser, to: id, from: id, map: { inline: false, annotation: false, }, }) .then(res => res.css); return { moduleJson, code: nextCode, }; } const pluginPre = () => { return { enforce: 'pre', name: 'vite-plugin-transform-css-modules-pre', configResolved(_config) { if (!_config || !_config.css) return; if (_config.css.modules) { const { modules } = _config.css; modulesOptions = Object.assign({}, modulesOptions, modules); } if (_config.css.postcss && _config.css.postcss.plugins) { const { plugins } = _config.css.postcss; if (Array.isArray(plugins)) { postcssPlugins.push(...plugins); } else { console.error('配置项 [css.postcss] 必须是数组类型'); } } }, async transform(raw, id) { if (cssModuleLangs.test(id) && !id.includes('node_modules')) { const { code, moduleJson } = await compileCSS(id, raw); // 导出模块化后的字符串给后置的插件使用 cssModuleJSON = moduleJson && dataToEsm(moduleJson, { namedExports: true, preferConst: true }); return { code, map: { mappings: '' }, }; } }, }; }; const pluginPost = () => { return { enforce: 'post', name: 'vite-plugin-transform-css-modules-post', async transform(css, id) { if (cssModuleLangs.test(id) && !id.includes('node_modules')) { // TODO: 暂时用的是文字截取方案,但每个Vite版本的变量不一致,有得包含__vite__前缀有的没有, const startStr = 'const __vite__css = '; // 'const css = ' const startEnd = '__vite__updateStyle(__vite__id, __vite__css)'; // 'updateStyle(id, css)' const cssCodeStartIndex = css.indexOf(startStr); const cssCodeEndIndex = css.indexOf(startEnd); const cssStr = css.slice(cssCodeStartIndex + startStr.length, cssCodeEndIndex); const pathIdx = id.indexOf('/src/'); const str = id.slice(pathIdx, id.length); return [ `import.meta.hot = __vite__createHotContext('${str}');`, `import { updateStyle as __vite__updateStyle, removeStyle } from "/@vite/client"`, `const __vite__id = ${JSON.stringify(id)}`, `const __vite__css = ${cssStr}`, `__vite__updateStyle(__vite__id, __vite__css)`, cssModuleJSON ? `${cssModuleJSON}` + `import.meta.hot.accept('${str}')` : 'import.meta.hot.accept()' + 'export default __vite__css', `import.meta.hot.prune(() => removeStyle(__vite__id))`, ].join('\n'); } }, }; }; /** * 可自定义文件路径的css module */ const vitePluginCssModule = (options) => { const { path, ...rest } = options || {}; if (path) { cssModuleLangs = path; } if (rest) { modulesOptions = Object.assign({}, modulesOptions, rest); } return [pluginPre(), pluginPost()]; }; export { vitePluginCssModule as default };