Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Remove scoping functionality and add scoping contract
  • Loading branch information
TrySound committed Jun 12, 2017
commit f163ee415c7b367c72914cec0e3e86ea311fa961
5 changes: 0 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,3 @@ node_js:
- "4"
- "6"
- "node"

after_success:
- cat ./coverage/lcov.info | node_modules/.bin/coveralls --verbose
- cat ./coverage/coverage.json | node_modules/codecov.io/bin/codecov.io.js
- rm -rf ./coverage
19 changes: 15 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,21 @@
},
"lint-staged": {
"*.js": [
"prettier --single-quote --no-semi --write",
"prettier --write",
"eslint",
"git add"
]
},
"eslintConfig": {
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"env": {
"es6": true
},
"extends": "eslint:recommended"
},
"babel": {
"presets": [
[
Expand Down Expand Up @@ -54,12 +65,12 @@
"babel-cli": "^6.24.1",
"babel-jest": "^20.0.3",
"babel-preset-env": "^1.5.1",
"codecov.io": "^0.1.2",
"coveralls": "^2.11.2",
"css-selector-parser": "^1.0.4",
"eslint": "^4.0.0",
"husky": "^0.13.3",
"jest": "^20.0.3",
"lint-staged": "^3.4.2",
"prettier": "^1.3.1"
"prettier": "^1.3.1",
"strip-indent": "^2.0.0"
}
}
264 changes: 106 additions & 158 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,172 +1,120 @@
import postcss from 'postcss'
import Tokenizer from 'css-selector-tokenizer'
import { extractICSS, createICSSRules } from 'icss-utils'
/* eslint-env node */
import postcss from "postcss";
import Tokenizer from "css-selector-tokenizer";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webpack/css-loader#523 Maybe in the long-term better replaced by e.g postcss-selector-parser but not sooo important now 😛

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like tokenizer as it produces only ast like value parser.

import { extractICSS, createICSSRules } from "icss-utils";

let hasOwnProperty = Object.prototype.hasOwnProperty
const plugin = "postcss-icss-composes";

function getSingleLocalNamesForComposes(selectors) {
return selectors.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) +
'"'
)
}
node = node.nodes[0]
if (
node.type !== 'nested-pseudo-class' ||
node.name !== 'local' ||
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]
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]
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) +
'", "' +
Tokenizer.stringify(node) +
'" is weird'
)
}
return node.name
})
}

const defaultGenerateScopedName = function(exportedName, path) {
let sanitisedPath = path
.replace(/\.[^\.\/\\]+$/, '')
.replace(/[\W_]+/g, '_')
.replace(/^_|_$/g, '')
return `_${sanitisedPath}__${exportedName}`
}
const isSingular = node => node.nodes.length === 1;

module.exports = postcss.plugin(
'postcss-modules-scope',
(options = {}) => css => {
let generateScopedName =
options.generateScopedName || defaultGenerateScopedName
const isLocal = node =>
node.type === "nested-pseudo-class" && node.name === "local";

let exports = {}
const isClass = node => node.type === "class";

function exportScopedName(name) {
let scopedName = generateScopedName(
name,
css.source.input.from,
css.source.input.css
)
exports[name] = exports[name] || []
if (exports[name].indexOf(scopedName) === -1) {
exports[name].push(scopedName)
}
return scopedName
const getSelectorIdentifier = selector => {
if (!isSingular(selector)) {
return null;
}
const [node] = selector.nodes;
if (isLocal(node)) {
const local = node.nodes[0];
if (isSingular(local) && isClass(local.nodes[0])) {
return local.nodes[0].name;
}
return null;
}
if (isClass(node)) {
return node.name;
}
return null;
};

function localizeNode(node) {
let newNode = Object.create(node)
switch (node.type) {
case 'selector':
newNode.nodes = node.nodes.map(localizeNode)
return newNode
case 'class':
case 'id':
let scopedName = exportScopedName(node.name)
newNode.name = scopedName
return newNode
const getIdentifiers = (rule, result) => {
const selectors = Tokenizer.parse(rule.selector).nodes;
return selectors
.map(selector => {
const identifier = getSelectorIdentifier(selector);
if (identifier === null) {
result.warn(
`composition is only allowed in single class selector, not in '${Tokenizer.stringify(selector)}'`,
{ node: rule }
);
}
throw new Error(
`${node.type} ("${Tokenizer.stringify(node)}") is not allowed in a :local block`
)
}
return identifier;
})
.filter(identifier => identifier !== null);
};

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')
}
return localizeNode(node.nodes[0])
}
/* falls through */
case 'selectors':
case 'selector':
let newNode = Object.create(node)
newNode.nodes = node.nodes.map(traverseNode)
return newNode
const walkComposes = (css, callback) =>
css.walkRules(rule => {
rule.each(node => {
if (node.type === "decl" && /^(composes|compose-with)$/.test(node.prop)) {
callback(rule, node);
}
return node
}
});
});

// Find any :import and remember imported names
const { icssImports } = extractICSS(css, false)
const importedNames = Object.keys(icssImports).reduce((acc, key) => {
Object.keys(icssImports[key]).forEach(local => {
acc[local] = true
})
return acc
}, {})
const flatten = outer => outer.reduce((acc, inner) => [...acc, ...inner], []);

// Find any :local classes
css.walkRules(rule => {
let selector = Tokenizer.parse(rule.selector)
let newSelector = traverseNode(selector)
rule.selector = Tokenizer.stringify(newSelector)
rule.walkDecls(/composes|compose-with/, decl => {
let localNames = getSingleLocalNamesForComposes(selector)
let classes = decl.value.split(/\s+/)
classes.forEach(className => {
let global = /^global\(([^\)]+)\)$/.exec(className)
if (global) {
localNames.forEach(exportedName => {
exports[exportedName].push(global[1])
})
} else if (hasOwnProperty.call(importedNames, className)) {
localNames.forEach(exportedName => {
exports[exportedName].push(className)
})
} else if (hasOwnProperty.call(exports, className)) {
localNames.forEach(exportedName => {
exports[className].forEach(item => {
exports[exportedName].push(item)
})
})
} else {
throw decl.error(
`referenced class name "${className}" in ${decl.prop} not found`
)
}
})
decl.remove()
})
})
const combineIntoMessages = (classes, composed) =>
flatten(
classes.map(name =>
composed.map(value => ({
plugin,
type: "icss-composed",
name,
value
}))
)
);

// If we found any :locals, insert an :export rule
const normalizedExports = Object.keys(exports).reduce((acc, key) => {
acc[key] = exports[key].join(' ')
return acc
}, {})
css.append(createICSSRules({}, normalizedExports))
}
)
const convertMessagesToExports = (messages, aliases) =>
messages
.map(msg => msg.name)
.reduce(
(acc, name) => (acc.indexOf(name) === -1 ? [...acc, name] : acc),
[]
)
.reduce(
(acc, name) =>
Object.assign({}, acc, {
[name]: [
aliases[name] || name,
...messages
.filter(msg => msg.name === name)
.map(msg => aliases[msg.value] || msg.value)
].join(" ")
}),
{}
);

const getScopedClasses = messages =>
messages
.filter(msg => msg.type === "icss-scoped")
.reduce(
(acc, msg) => Object.assign({}, acc, { [msg.name]: msg.value }),
{}
);

module.exports = postcss.plugin(plugin, () => (css, result) => {
const scopedClasses = getScopedClasses(result.messages);
const composedMessages = [];

const { icssImports, icssExports } = extractICSS(css);

walkComposes(css, (rule, decl) => {
const classes = getIdentifiers(rule, result);
const composed = decl.value.split(/\s+/);
composedMessages.push(...combineIntoMessages(classes, composed));
decl.remove();
});

module.exports.generateScopedName = defaultGenerateScopedName
const compositionExports = convertMessagesToExports(
composedMessages,
scopedClasses
);
const exports = Object.assign({}, icssExports, compositionExports);
css.prepend(createICSSRules(icssImports, exports));
result.messages.push(...composedMessages);
});
1 change: 0 additions & 1 deletion test/test-cases/error-comma-in-local/expected.error.txt

This file was deleted.

3 changes: 0 additions & 3 deletions test/test-cases/error-comma-in-local/source.css

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

3 changes: 0 additions & 3 deletions test/test-cases/error-composes-not-defined-class/source.css

This file was deleted.

This file was deleted.

3 changes: 0 additions & 3 deletions test/test-cases/error-not-allowed-in-local/source.css

This file was deleted.

11 changes: 0 additions & 11 deletions test/test-cases/export-child-class/expected.css

This file was deleted.

7 changes: 0 additions & 7 deletions test/test-cases/export-child-class/source.css

This file was deleted.

3 changes: 0 additions & 3 deletions test/test-cases/export-class-path/config.json

This file was deleted.

Loading