0d5f2cfc6dce071616790d90757451641bee6a38fd71147186e2521f7edc41efc064621cc8e5c65a0ba7669a60c78fa809a6d3e82536121d88434f7d407bef 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. 'use strict';
  2. const postcss = require('postcss');
  3. const selectorParser = require('postcss-selector-parser');
  4. const hasOwnProperty = Object.prototype.hasOwnProperty;
  5. function getSingleLocalNamesForComposes(root) {
  6. return root.nodes.map(node => {
  7. if (node.type !== 'selector' || node.nodes.length !== 1) {
  8. throw new Error(
  9. `composition is only allowed when selector is single :local class name not in "${root}"`
  10. );
  11. }
  12. node = node.nodes[0];
  13. if (
  14. node.type !== 'pseudo' ||
  15. node.value !== ':local' ||
  16. node.nodes.length !== 1
  17. ) {
  18. throw new Error(
  19. 'composition is only allowed when selector is single :local class name not in "' +
  20. root +
  21. '", "' +
  22. node +
  23. '" is weird'
  24. );
  25. }
  26. node = node.first;
  27. if (node.type !== 'selector' || node.length !== 1) {
  28. throw new Error(
  29. 'composition is only allowed when selector is single :local class name not in "' +
  30. root +
  31. '", "' +
  32. node +
  33. '" is weird'
  34. );
  35. }
  36. node = node.first;
  37. if (node.type !== 'class') {
  38. // 'id' is not possible, because you can't compose ids
  39. throw new Error(
  40. 'composition is only allowed when selector is single :local class name not in "' +
  41. root +
  42. '", "' +
  43. node +
  44. '" is weird'
  45. );
  46. }
  47. return node.value;
  48. });
  49. }
  50. const whitespace = '[\\x20\\t\\r\\n\\f]';
  51. const unescapeRegExp = new RegExp(
  52. '\\\\([\\da-f]{1,6}' + whitespace + '?|(' + whitespace + ')|.)',
  53. 'ig'
  54. );
  55. function unescape(str) {
  56. return str.replace(unescapeRegExp, (_, escaped, escapedWhitespace) => {
  57. const high = '0x' + escaped - 0x10000;
  58. // NaN means non-codepoint
  59. // Workaround erroneous numeric interpretation of +"0x"
  60. return high !== high || escapedWhitespace
  61. ? escaped
  62. : high < 0
  63. ? // BMP codepoint
  64. String.fromCharCode(high + 0x10000)
  65. : // Supplemental Plane codepoint (surrogate pair)
  66. String.fromCharCode((high >> 10) | 0xd800, (high & 0x3ff) | 0xdc00);
  67. });
  68. }
  69. const processor = postcss.plugin('postcss-modules-scope', function(options) {
  70. return css => {
  71. const generateScopedName =
  72. (options && options.generateScopedName) || processor.generateScopedName;
  73. const generateExportEntry =
  74. (options && options.generateExportEntry) || processor.generateExportEntry;
  75. const exportGlobals = options && options.exportGlobals;
  76. const exports = Object.create(null);
  77. function exportScopedName(name, rawName) {
  78. const scopedName = generateScopedName(
  79. rawName ? rawName : name,
  80. css.source.input.from,
  81. css.source.input.css
  82. );
  83. const exportEntry = generateExportEntry(
  84. rawName ? rawName : name,
  85. scopedName,
  86. css.source.input.from,
  87. css.source.input.css
  88. );
  89. const { key, value } = exportEntry;
  90. exports[key] = exports[key] || [];
  91. if (exports[key].indexOf(value) < 0) {
  92. exports[key].push(value);
  93. }
  94. return scopedName;
  95. }
  96. function localizeNode(node) {
  97. switch (node.type) {
  98. case 'selector':
  99. node.nodes = node.map(localizeNode);
  100. return node;
  101. case 'class':
  102. return selectorParser.className({
  103. value: exportScopedName(
  104. node.value,
  105. node.raws && node.raws.value ? node.raws.value : null
  106. ),
  107. });
  108. case 'id': {
  109. return selectorParser.id({
  110. value: exportScopedName(
  111. node.value,
  112. node.raws && node.raws.value ? node.raws.value : null
  113. ),
  114. });
  115. }
  116. }
  117. throw new Error(
  118. `${node.type} ("${node}") is not allowed in a :local block`
  119. );
  120. }
  121. function traverseNode(node) {
  122. switch (node.type) {
  123. case 'pseudo':
  124. if (node.value === ':local') {
  125. if (node.nodes.length !== 1) {
  126. throw new Error('Unexpected comma (",") in :local block');
  127. }
  128. const selector = localizeNode(node.first, node.spaces);
  129. // move the spaces that were around the psuedo selector to the first
  130. // non-container node
  131. selector.first.spaces = node.spaces;
  132. const nextNode = node.next();
  133. if (
  134. nextNode &&
  135. nextNode.type === 'combinator' &&
  136. nextNode.value === ' ' &&
  137. /\\[A-F0-9]{1,6}$/.test(selector.last.value)
  138. ) {
  139. selector.last.spaces.after = ' ';
  140. }
  141. node.replaceWith(selector);
  142. return;
  143. }
  144. /* falls through */
  145. case 'root':
  146. case 'selector': {
  147. node.each(traverseNode);
  148. break;
  149. }
  150. case 'id':
  151. case 'class':
  152. if (exportGlobals) {
  153. exports[node.value] = [node.value];
  154. }
  155. break;
  156. }
  157. return node;
  158. }
  159. // Find any :import and remember imported names
  160. const importedNames = {};
  161. css.walkRules(rule => {
  162. if (/^:import\(.+\)$/.test(rule.selector)) {
  163. rule.walkDecls(decl => {
  164. importedNames[decl.prop] = true;
  165. });
  166. }
  167. });
  168. // Find any :local classes
  169. css.walkRules(rule => {
  170. if (
  171. rule.nodes &&
  172. rule.selector.slice(0, 2) === '--' &&
  173. rule.selector.slice(-1) === ':'
  174. ) {
  175. // ignore custom property set
  176. return;
  177. }
  178. let parsedSelector = selectorParser().astSync(rule);
  179. rule.selector = traverseNode(parsedSelector.clone()).toString();
  180. rule.walkDecls(/composes|compose-with/, decl => {
  181. const localNames = getSingleLocalNamesForComposes(parsedSelector);
  182. const classes = decl.value.split(/\s+/);
  183. classes.forEach(className => {
  184. const global = /^global\(([^\)]+)\)$/.exec(className);
  185. if (global) {
  186. localNames.forEach(exportedName => {
  187. exports[exportedName].push(global[1]);
  188. });
  189. } else if (hasOwnProperty.call(importedNames, className)) {
  190. localNames.forEach(exportedName => {
  191. exports[exportedName].push(className);
  192. });
  193. } else if (hasOwnProperty.call(exports, className)) {
  194. localNames.forEach(exportedName => {
  195. exports[className].forEach(item => {
  196. exports[exportedName].push(item);
  197. });
  198. });
  199. } else {
  200. throw decl.error(
  201. `referenced class name "${className}" in ${decl.prop} not found`
  202. );
  203. }
  204. });
  205. decl.remove();
  206. });
  207. rule.walkDecls(decl => {
  208. let tokens = decl.value.split(/(,|'[^']*'|"[^"]*")/);
  209. tokens = tokens.map((token, idx) => {
  210. if (idx === 0 || tokens[idx - 1] === ',') {
  211. const localMatch = /^(\s*):local\s*\((.+?)\)/.exec(token);
  212. if (localMatch) {
  213. return (
  214. localMatch[1] +
  215. exportScopedName(localMatch[2]) +
  216. token.substr(localMatch[0].length)
  217. );
  218. } else {
  219. return token;
  220. }
  221. } else {
  222. return token;
  223. }
  224. });
  225. decl.value = tokens.join('');
  226. });
  227. });
  228. // Find any :local keyframes
  229. css.walkAtRules(atrule => {
  230. if (/keyframes$/i.test(atrule.name)) {
  231. const localMatch = /^\s*:local\s*\((.+?)\)\s*$/.exec(atrule.params);
  232. if (localMatch) {
  233. atrule.params = exportScopedName(localMatch[1]);
  234. }
  235. }
  236. });
  237. // If we found any :locals, insert an :export rule
  238. const exportedNames = Object.keys(exports);
  239. if (exportedNames.length > 0) {
  240. const exportRule = postcss.rule({ selector: ':export' });
  241. exportedNames.forEach(exportedName =>
  242. exportRule.append({
  243. prop: exportedName,
  244. value: exports[exportedName].join(' '),
  245. raws: { before: '\n ' },
  246. })
  247. );
  248. css.append(exportRule);
  249. }
  250. };
  251. });
  252. processor.generateScopedName = function(name, path) {
  253. const sanitisedPath = path
  254. .replace(/\.[^\.\/\\]+$/, '')
  255. .replace(/[\W_]+/g, '_')
  256. .replace(/^_|_$/g, '');
  257. return `_${sanitisedPath}__${name}`.trim();
  258. };
  259. processor.generateExportEntry = function(name, scopedName) {
  260. return {
  261. key: unescape(name),
  262. value: unescape(scopedName),
  263. };
  264. };
  265. module.exports = processor;