diff --git a/package.json b/package.json index c169704..a7bb4e2 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "precover": "yarn lint", "cover": "nyc mocha", "travis": "yarn cover", - "prepublish": "yarn run test" + "prepublishOnly": "yarn run test" }, "repository": { "type": "git", @@ -36,7 +36,8 @@ "homepage": "https://github.com/css-modules/postcss-modules-scope", "dependencies": { "css-selector-tokenizer": "^0.7.0", - "postcss": "^7.0.6" + "postcss": "^7.0.6", + "postcss-modules-values": "^2.0.0" }, "devDependencies": { "chokidar-cli": "^1.0.1", @@ -44,7 +45,7 @@ "coveralls": "^3.0.2", "css-selector-parser": "^1.0.4", "eslint": "^5.9.0", - "nyc": "^13.1.0", - "mocha": "^5.2.0" + "mocha": "^5.2.0", + "nyc": "^13.1.0" } } diff --git a/src/index.js b/src/index.js index 9fede98..41e4d1f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,9 @@ -'use strict'; +'use strict' -const postcss = require('postcss'); -const Tokenizer = require('css-selector-tokenizer'); +const postcss = require('postcss') +const Tokenizer = require('css-selector-tokenizer') -const hasOwnProperty = Object.prototype.hasOwnProperty; +const hasOwnProperty = Object.prototype.hasOwnProperty function getSingleLocalNamesForComposes(selectors) { return selectors.nodes.map(node => { @@ -12,9 +12,9 @@ function getSingleLocalNamesForComposes(selectors) { 'composition is only allowed when selector is single :local class name not in "' + Tokenizer.stringify(selectors) + '"' - ); + ) } - node = node.nodes[0]; + node = node.nodes[0] if ( node.type !== 'nested-pseudo-class' || node.name !== 'local' || @@ -26,9 +26,9 @@ function getSingleLocalNamesForComposes(selectors) { '", "' + Tokenizer.stringify(node) + '" is weird' - ); + ) } - node = node.nodes[0]; + node = node.nodes[0] if (node.type !== 'selector' || node.nodes.length !== 1) { throw new Error( 'composition is only allowed when selector is single :local class name not in "' + @@ -36,9 +36,9 @@ function getSingleLocalNamesForComposes(selectors) { '", "' + Tokenizer.stringify(node) + '" is weird' - ); + ) } - node = node.nodes[0]; + node = node.nodes[0] if (node.type !== 'class') { // 'id' is not possible, because you can't compose ids throw new Error( @@ -47,42 +47,52 @@ function getSingleLocalNamesForComposes(selectors) { '", "' + Tokenizer.stringify(node) + '" is weird' - ); + ) } - return node.name; - }); + return node.name + }) } + const processor = postcss.plugin('postcss-modules-scope', function(options) { return css => { const generateScopedName = - (options && options.generateScopedName) || processor.generateScopedName; + (options && options.generateScopedName) || processor.generateScopedName + + const exports = Object.create(null) - const exports = Object.create(null); + // Find any :import and remember imported names + const importedNames = Object.create(null) function exportScopedName(name) { + // Don't change selectors that have already been replaced by a value import + // eg: + // @value foo from './foo' + // .bar .foo { color: red; } + if (importedNames[name]) return name; + const scopedName = generateScopedName( name, css.source.input.from, css.source.input.css - ); - exports[name] = exports[name] || []; + ) + exports[name] = exports[name] || [] if (exports[name].indexOf(scopedName) < 0) { - exports[name].push(scopedName); + exports[name].push(scopedName) } - return scopedName; + return scopedName } function localizeNode(node) { - const newNode = Object.create(node); + const newNode = Object.create(node) switch (node.type) { case 'selector': - newNode.nodes = node.nodes.map(localizeNode); - return newNode; + newNode.nodes = node.nodes.map(localizeNode) + return newNode case 'class': case 'id': { - newNode.name = exportScopedName(node.name); - return newNode; + newNode.name = exportScopedName(node.name) + return newNode } } throw new Error( @@ -90,7 +100,7 @@ const processor = postcss.plugin('postcss-modules-scope', function(options) { ' ("' + Tokenizer.stringify(node) + '") is not allowed in a :local block' - ); + ) } function traverseNode(node) { @@ -98,118 +108,117 @@ const processor = postcss.plugin('postcss-modules-scope', function(options) { case 'nested-pseudo-class': if (node.name === 'local') { if (node.nodes.length !== 1) { - throw new Error('Unexpected comma (",") in :local block'); + throw new Error('Unexpected comma (",") in :local block') } - return localizeNode(node.nodes[0]); + return localizeNode(node.nodes[0]) } /* falls through */ case 'selectors': case 'selector': { - const newNode = Object.create(node); - newNode.nodes = node.nodes.map(traverseNode); - return newNode; + const newNode = Object.create(node) + newNode.nodes = node.nodes.map(traverseNode) + return newNode } } - return node; + return node } - // Find any :import and remember imported names - const importedNames = {}; + css.walkRules(rule => { if (/^:import\(.+\)$/.test(rule.selector)) { rule.walkDecls(decl => { - importedNames[decl.prop] = true; - }); + importedNames[decl.prop] = true + }) } - }); + }) // Find any :local classes css.walkRules(rule => { - const selector = Tokenizer.parse(rule.selector); - const newSelector = traverseNode(selector); - rule.selector = Tokenizer.stringify(newSelector); + const selector = Tokenizer.parse(rule.selector) + const newSelector = traverseNode(selector) + rule.selector = Tokenizer.stringify(newSelector) rule.walkDecls(/composes|compose-with/, decl => { - const localNames = getSingleLocalNamesForComposes(selector); - const classes = decl.value.split(/\s+/); + const localNames = getSingleLocalNamesForComposes(selector) + const classes = decl.value.split(/\s+/) classes.forEach(className => { - const global = /^global\(([^\)]+)\)$/.exec(className); + const global = /^global\(([^\)]+)\)$/.exec(className) if (global) { localNames.forEach(exportedName => { - exports[exportedName].push(global[1]); - }); + exports[exportedName].push(global[1]) + }) } else if (hasOwnProperty.call(importedNames, className)) { localNames.forEach(exportedName => { - exports[exportedName].push(className); - }); + exports[exportedName].push(className) + }) } else if (hasOwnProperty.call(exports, className)) { localNames.forEach(exportedName => { exports[className].forEach(item => { - exports[exportedName].push(item); - }); - }); + exports[exportedName].push(item) + }) + }) } else { throw decl.error( `referenced class name "${className}" in ${decl.prop} not found` - ); + ) } - }); - decl.remove(); - }); + }) + decl.remove() + }) rule.walkDecls(decl => { - var tokens = decl.value.split(/(,|'[^']*'|"[^"]*")/); + var tokens = decl.value.split(/(,|'[^']*'|"[^"]*")/) tokens = tokens.map((token, idx) => { if (idx === 0 || tokens[idx - 1] === ',') { - const localMatch = /^(\s*):local\s*\((.+?)\)/.exec(token); + const localMatch = /^(\s*):local\s*\((.+?)\)/.exec(token) if (localMatch) { return ( localMatch[1] + exportScopedName(localMatch[2]) + token.substr(localMatch[0].length) - ); + ) } else { - return token; + return token } } else { - return token; + return token } - }); - decl.value = tokens.join(''); - }); - }); + }) + decl.value = tokens.join('') + }) + }) // Find any :local keyframes css.walkAtRules(atrule => { if (/keyframes$/i.test(atrule.name)) { - var localMatch = /^\s*:local\s*\((.+?)\)\s*$/.exec(atrule.params); + var localMatch = /^\s*:local\s*\((.+?)\)\s*$/.exec(atrule.params) if (localMatch) { - atrule.params = exportScopedName(localMatch[1]); + atrule.params = exportScopedName(localMatch[1]) } } - }); + }) // If we found any :locals, insert an :export rule - const exportedNames = Object.keys(exports); + const exportedNames = Object.keys(exports) if (exportedNames.length > 0) { - const exportRule = postcss.rule({ selector: ':export' }); + const exportRule = postcss.rule({ selector: ':export' }) exportedNames.forEach(exportedName => exportRule.append({ prop: exportedName, value: exports[exportedName].join(' '), - raws: { before: '\n ' } + raws: { before: '\n ' }, }) - ); - css.append(exportRule); + ) + css.append(exportRule) } - }; -}); + } +}) processor.generateScopedName = function(exportedName, path) { const sanitisedPath = path .replace(/\.[^\.\/\\]+$/, '') .replace(/[\W_]+/g, '_') - .replace(/^_|_$/g, ''); - return `_${sanitisedPath}__${exportedName}`; -}; + .replace(/^_|_$/g, '') + return `_${sanitisedPath}__${exportedName}` +} -module.exports = processor; +module.exports = processor diff --git a/test/test-cases/export-with-imported-class/config.json b/test/test-cases/export-with-imported-class/config.json new file mode 100644 index 0000000..57a5f96 --- /dev/null +++ b/test/test-cases/export-with-imported-class/config.json @@ -0,0 +1,3 @@ +{ + "from": "/lib/extender.css" +} \ No newline at end of file diff --git a/test/test-cases/export-with-imported-class/expected.css b/test/test-cases/export-with-imported-class/expected.css new file mode 100644 index 0000000..d1209bf --- /dev/null +++ b/test/test-cases/export-with-imported-class/expected.css @@ -0,0 +1,9 @@ +:import("./file.css") { + imported_otherClass: otherClass; +} +._lib_extender__exportName > .imported_otherClass { + color: green; +} +:export { + exportName: _lib_extender__exportName; +} diff --git a/test/test-cases/export-with-imported-class/source.css b/test/test-cases/export-with-imported-class/source.css new file mode 100644 index 0000000..b68be81 --- /dev/null +++ b/test/test-cases/export-with-imported-class/source.css @@ -0,0 +1,6 @@ +:import("./file.css") { + imported_otherClass: otherClass; +} +:local(.exportName) > :local(.imported_otherClass) { + color: green; +} diff --git a/yarn.lock b/yarn.lock index 2c0337d..efdd7bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1161,6 +1161,11 @@ iconv-lite@^0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= + ignore-walk@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" @@ -2031,6 +2036,14 @@ posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" +postcss-modules-values@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64" + integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w== + dependencies: + icss-replace-symbols "^1.1.0" + postcss "^7.0.6" + postcss@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.6.tgz#6dcaa1e999cdd4a255dcd7d4d9547f4ca010cdc2"