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