diff --git a/package.json b/package.json index c169704..ca4304d 100644 --- a/package.json +++ b/package.json @@ -34,17 +34,21 @@ "url": "https://github.com/css-modules/postcss-modules-scope/issues" }, "homepage": "https://github.com/css-modules/postcss-modules-scope", + "prettier": { + "semi": true, + "singleQuote": true, + "trailingComma": "es5" + }, "dependencies": { - "css-selector-tokenizer": "^0.7.0", - "postcss": "^7.0.6" + "postcss": "^7.0.6", + "postcss-selector-parser": "^5.0.0" }, "devDependencies": { "chokidar-cli": "^1.0.1", "codecov.io": "^0.1.2", "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..709b1cd 100644 --- a/src/index.js +++ b/src/index.js @@ -1,55 +1,53 @@ 'use strict'; const postcss = require('postcss'); -const Tokenizer = require('css-selector-tokenizer'); +const selectorParser = require('postcss-selector-parser'); const hasOwnProperty = Object.prototype.hasOwnProperty; -function getSingleLocalNamesForComposes(selectors) { - return selectors.nodes.map(node => { +function getSingleLocalNamesForComposes(root) { + return root.nodes.map(node => { if (node.type !== 'selector' || node.nodes.length !== 1) { throw new Error( - 'composition is only allowed when selector is single :local class name not in "' + - Tokenizer.stringify(selectors) + - '"' + `composition is only allowed when selector is single :local class name not in "${root}"` ); } node = node.nodes[0]; if ( - node.type !== 'nested-pseudo-class' || - node.name !== 'local' || + node.type !== 'pseudo' || + node.value !== ':local' || node.nodes.length !== 1 ) { throw new Error( 'composition is only allowed when selector is single :local class name not in "' + - Tokenizer.stringify(selectors) + + root + '", "' + - Tokenizer.stringify(node) + + node + '" is weird' ); } - node = node.nodes[0]; - if (node.type !== 'selector' || node.nodes.length !== 1) { + node = node.first; + if (node.type !== 'selector' || node.length !== 1) { throw new Error( 'composition is only allowed when selector is single :local class name not in "' + - Tokenizer.stringify(selectors) + + root + '", "' + - Tokenizer.stringify(node) + + node + '" is weird' ); } - node = node.nodes[0]; + node = node.first; if (node.type !== 'class') { // 'id' is not possible, because you can't compose ids throw new Error( 'composition is only allowed when selector is single :local class name not in "' + - Tokenizer.stringify(selectors) + + root + '", "' + - Tokenizer.stringify(node) + + node + '" is weird' ); } - return node.name; + return node.value; }); } @@ -74,40 +72,45 @@ const processor = postcss.plugin('postcss-modules-scope', function(options) { } function localizeNode(node) { - const newNode = Object.create(node); switch (node.type) { case 'selector': - newNode.nodes = node.nodes.map(localizeNode); - return newNode; + node.nodes = node.map(localizeNode); + return node; case 'class': + return selectorParser.className({ + value: exportScopedName(node.value), + }); case 'id': { - newNode.name = exportScopedName(node.name); - return newNode; + return selectorParser.id({ + value: exportScopedName(node.value), + }); } } throw new Error( - node.type + - ' ("' + - Tokenizer.stringify(node) + - '") is not allowed in a :local block' + `${node.type} ("${node}") is not allowed in a :local block` ); } function traverseNode(node) { switch (node.type) { - case 'nested-pseudo-class': - if (node.name === 'local') { + case 'pseudo': + if (node.value === ':local') { if (node.nodes.length !== 1) { throw new Error('Unexpected comma (",") in :local block'); } - return localizeNode(node.nodes[0]); + const selector = localizeNode(node.first, node.spaces); + // move the spaces that were around the psuedo selector to the first + // non-container node + selector.first.spaces = node.spaces; + + node.replaceWith(selector); + return; } /* falls through */ - case 'selectors': + case 'root': case 'selector': { - const newNode = Object.create(node); - newNode.nodes = node.nodes.map(traverseNode); - return newNode; + node.each(traverseNode); + break; } } return node; @@ -125,14 +128,16 @@ const processor = postcss.plugin('postcss-modules-scope', function(options) { // Find any :local classes css.walkRules(rule => { - const selector = Tokenizer.parse(rule.selector); - const newSelector = traverseNode(selector); - rule.selector = Tokenizer.stringify(newSelector); + let parsedSelector = selectorParser().astSync(rule); + + rule.selector = traverseNode(parsedSelector.clone()).toString(); + // console.log(rule.selector); rule.walkDecls(/composes|compose-with/, decl => { - const localNames = getSingleLocalNamesForComposes(selector); + const localNames = getSingleLocalNamesForComposes(parsedSelector); const classes = decl.value.split(/\s+/); classes.forEach(className => { const global = /^global\(([^\)]+)\)$/.exec(className); + if (global) { localNames.forEach(exportedName => { exports[exportedName].push(global[1]); @@ -196,7 +201,7 @@ const processor = postcss.plugin('postcss-modules-scope', function(options) { exportRule.append({ prop: exportedName, value: exports[exportedName].join(' '), - raws: { before: '\n ' } + raws: { before: '\n ' }, }) ); css.append(exportRule); @@ -209,7 +214,7 @@ processor.generateScopedName = function(exportedName, path) { .replace(/\.[^\.\/\\]+$/, '') .replace(/[\W_]+/g, '_') .replace(/^_|_$/g, ''); - return `_${sanitisedPath}__${exportedName}`; + return `_${sanitisedPath}__${exportedName}`.trim(); }; module.exports = processor; diff --git a/test/test-cases.js b/test/test-cases.js index 30263c2..afe3152 100644 --- a/test/test-cases.js +++ b/test/test-cases.js @@ -23,31 +23,48 @@ var generateScopedName = processor.generateScopedName; describe('test-cases', function() { var testDir = path.join(__dirname, 'test-cases'); fs.readdirSync(testDir).forEach(function(testCase) { - if(fs.existsSync(path.join(testDir, testCase, 'source.css'))) { + if (fs.existsSync(path.join(testDir, testCase, 'source.css'))) { it('should ' + testCase.replace(/-/g, ' '), function() { - var input = normalize(fs.readFileSync(path.join(testDir, testCase, 'source.css'), 'utf-8')); + var input = normalize( + fs.readFileSync(path.join(testDir, testCase, 'source.css'), 'utf-8') + ); + var expected, expectedError; - if(fs.existsSync(path.join(testDir, testCase, 'expected.error.txt'))) { - expectedError = normalize(fs.readFileSync(path.join(testDir, testCase, 'expected.error.txt'), 'utf-8')) - .split('\n')[0]; + if (fs.existsSync(path.join(testDir, testCase, 'expected.error.txt'))) { + expectedError = normalize( + fs.readFileSync( + path.join(testDir, testCase, 'expected.error.txt'), + 'utf-8' + ) + ).split('\n')[0]; } else { - expected = normalize(fs.readFileSync(path.join(testDir, testCase, 'expected.css'), 'utf-8')); + expected = normalize( + fs.readFileSync( + path.join(testDir, testCase, 'expected.css'), + 'utf-8' + ) + ); } var config = { from: '/input' }; var options = { generateScopedName: function(exportedName, inputPath) { var normalizedPath = inputPath.replace(/^[A-Z]:/, ''); return generateScopedName(exportedName, normalizedPath); - } + }, }; - if(fs.existsSync(path.join(testDir, testCase, 'config.json'))) { - config = JSON.parse(fs.readFileSync(path.join(testDir, testCase, 'config.json'), 'utf-8')); + if (fs.existsSync(path.join(testDir, testCase, 'config.json'))) { + config = JSON.parse( + fs.readFileSync( + path.join(testDir, testCase, 'config.json'), + 'utf-8' + ) + ); } - if(fs.existsSync(path.join(testDir, testCase, 'options.js'))) { + if (fs.existsSync(path.join(testDir, testCase, 'options.js'))) { options = require(path.join(testDir, testCase, 'options.js')); } var pipeline = postcss([generateInvalidCSS, processor(options)]); - if(expectedError) { + if (expectedError) { assert.throws(function() { // eslint-ignore-next-line no-unused-vars const result = pipeline.process(input, config).css; diff --git a/test/test-cases/error-not-allowed-in-local/expected.error.txt b/test/test-cases/error-not-allowed-in-local/expected.error.txt index e5432ab..05dcac5 100644 --- a/test/test-cases/error-not-allowed-in-local/expected.error.txt +++ b/test/test-cases/error-not-allowed-in-local/expected.error.txt @@ -1 +1 @@ -element \("body"\) is not allowed in a :local block +tag \("body"\) is not allowed in a :local block diff --git a/test/test-cases/options-generateScopedName/options.js b/test/test-cases/options-generateScopedName/options.js index 071a9d4..9426036 100644 --- a/test/test-cases/options-generateScopedName/options.js +++ b/test/test-cases/options-generateScopedName/options.js @@ -1,5 +1,5 @@ module.exports = { generateScopedName: function(name, path) { - return '_' + name + '_'; - } + return '_' + name + '_'; + }, }; diff --git a/yarn.lock b/yarn.lock index 2c0337d..3a18852 100644 --- a/yarn.lock +++ b/yarn.lock @@ -541,21 +541,10 @@ cryptiles@0.2.x: dependencies: boom "0.4.x" -css-selector-parser@^1.0.4: - version "1.3.0" - resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.3.0.tgz#5f1ad43e2d8eefbfdc304fcd39a521664943e3eb" - -css-selector-tokenizer@^0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz#a177271a8bca5019172f4f891fc6eed9cbf68d5d" - dependencies: - cssesc "^0.1.0" - fastparse "^1.1.1" - regexpu-core "^1.0.0" - -cssesc@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" +cssesc@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" + integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== ctype@0.5.3: version "0.5.3" @@ -873,10 +862,6 @@ fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" -fastparse@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" - figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -1175,6 +1160,11 @@ imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1431,10 +1421,6 @@ jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" -jsesc@~0.5.0: - version "0.5.0" - resolved "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -2031,6 +2017,15 @@ 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-selector-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" + integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== + dependencies: + cssesc "^2.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + postcss@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.6.tgz#6dcaa1e999cdd4a255dcd7d4d9547f4ca010cdc2" @@ -2128,10 +2123,6 @@ readdirp@^2.0.0: micromatch "^3.1.10" readable-stream "^2.0.2" -regenerate@^1.2.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" - regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -2143,24 +2134,6 @@ regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" -regexpu-core@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" - dependencies: - regenerate "^1.2.1" - regjsgen "^0.2.0" - regjsparser "^0.1.4" - -regjsgen@^0.2.0: - version "0.2.0" - resolved "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" - -regjsparser@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" - dependencies: - jsesc "~0.5.0" - release-zalgo@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730" @@ -2681,6 +2654,11 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^0.4.3" +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"