Skip to content

fix: don't rescope imported names #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"precover": "yarn lint",
"cover": "nyc mocha",
"travis": "yarn cover",
"prepublish": "yarn run test"
"prepublishOnly": "yarn run test"
},
"repository": {
"type": "git",
Expand All @@ -36,15 +36,16 @@
"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",
"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"
}
}
161 changes: 85 additions & 76 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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 => {
Expand All @@ -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' ||
Expand All @@ -26,19 +26,19 @@ 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 "' +
Tokenizer.stringify(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(
Expand All @@ -47,169 +47,178 @@ 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(
node.type +
' ("' +
Tokenizer.stringify(node) +
'") is not allowed in a :local block'
);
)
}

function traverseNode(node) {
switch (node.type) {
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
3 changes: 3 additions & 0 deletions test/test-cases/export-with-imported-class/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"from": "/lib/extender.css"
}
9 changes: 9 additions & 0 deletions test/test-cases/export-with-imported-class/expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
:import("./file.css") {
imported_otherClass: otherClass;
}
._lib_extender__exportName > .imported_otherClass {
color: green;
}
:export {
exportName: _lib_extender__exportName;
}
6 changes: 6 additions & 0 deletions test/test-cases/export-with-imported-class/source.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:import("./file.css") {
imported_otherClass: otherClass;
}
:local(.exportName) > :local(.imported_otherClass) {
color: green;
}
13 changes: 13 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down