postcss-nested
Advanced tools
Comparing version 5.0.6 to 6.0.0
@@ -9,4 +9,4 @@ // Original definitions (@types/postcss-nested) | ||
/** | ||
* By default, plugin will bubble only `@media` and `@supports` at-rules. | ||
* You can add your custom at-rules to this list by this option. | ||
* By default, plugin will bubble only `@media`, `@supports` and `@layer` | ||
* at-rules. Use this option to add your custom at-rules to this list. | ||
*/ | ||
@@ -28,2 +28,9 @@ bubble?: string[] | ||
preserveEmpty?: boolean | ||
/** | ||
* The plugin supports the SCSS custom at-rule `@at-root` which breaks | ||
* rule blocks out of their nested position. If you want, you can choose | ||
* a new custom name for this rule in your code. | ||
*/ | ||
rootRuleName?: string | ||
} | ||
@@ -30,0 +37,0 @@ |
316
index.js
@@ -0,12 +1,15 @@ | ||
const { Rule, AtRule } = require('postcss') | ||
let parser = require('postcss-selector-parser') | ||
function parse (str, rule) { | ||
/** | ||
* Run a selector string through postcss-selector-parser | ||
*/ | ||
function parse(rawSelector, rule) { | ||
let nodes | ||
let saver = parser(parsed => { | ||
nodes = parsed | ||
}) | ||
try { | ||
saver.processSync(str) | ||
parser(parsed => { | ||
nodes = parsed | ||
}).processSync(rawSelector) | ||
} catch (e) { | ||
if (str.includes(':')) { | ||
if (rawSelector.includes(':')) { | ||
throw rule ? rule.error('Missed semicolon') : e | ||
@@ -20,15 +23,23 @@ } else { | ||
function replace (nodes, parent) { | ||
/** | ||
* Replaces the "&" token in a node's selector with the parent selector | ||
* similar to what SCSS does. | ||
* | ||
* Mutates the nodes list | ||
*/ | ||
function interpolateAmpInSelector(nodes, parent) { | ||
let replaced = false | ||
nodes.each(i => { | ||
if (i.type === 'nesting') { | ||
let clonedParent = parent.clone() | ||
if (i.value !== '&') { | ||
i.replaceWith(parse(i.value.replace('&', clonedParent.toString()))) | ||
nodes.each(node => { | ||
if (node.type === 'nesting') { | ||
let clonedParent = parent.clone({}) | ||
if (node.value !== '&') { | ||
node.replaceWith( | ||
parse(node.value.replace('&', clonedParent.toString())) | ||
) | ||
} else { | ||
i.replaceWith(clonedParent) | ||
node.replaceWith(clonedParent) | ||
} | ||
replaced = true | ||
} else if (i.nodes) { | ||
if (replace(i, parent)) { | ||
} else if ('nodes' in node && node.nodes) { | ||
if (interpolateAmpInSelector(node, parent)) { | ||
replaced = true | ||
@@ -41,47 +52,56 @@ } | ||
function selectors (parent, child) { | ||
let result = [] | ||
parent.selectors.forEach(i => { | ||
let parentNode = parse(i, parent) | ||
/** | ||
* Combines parent and child selectors, in a SCSS-like way | ||
*/ | ||
function mergeSelectors(parent, child) { | ||
let merged = [] | ||
parent.selectors.forEach(sel => { | ||
let parentNode = parse(sel, parent) | ||
child.selectors.forEach(j => { | ||
if (j.length) { | ||
let node = parse(j, child) | ||
let replaced = replace(node, parentNode) | ||
if (!replaced) { | ||
node.prepend(parser.combinator({ value: ' ' })) | ||
node.prepend(parentNode.clone()) | ||
} | ||
result.push(node.toString()) | ||
child.selectors.forEach(selector => { | ||
if (!selector) { | ||
return | ||
} | ||
let node = parse(selector, child) | ||
let replaced = interpolateAmpInSelector(node, parentNode) | ||
if (!replaced) { | ||
node.prepend(parser.combinator({ value: ' ' })) | ||
node.prepend(parentNode.clone({})) | ||
} | ||
merged.push(node.toString()) | ||
}) | ||
}) | ||
return result | ||
return merged | ||
} | ||
function pickComment (comment, after) { | ||
if (comment && comment.type === 'comment') { | ||
after.after(comment) | ||
return comment | ||
} else { | ||
return after | ||
/** | ||
* Move a child and its preceeding comment(s) to after "after" | ||
*/ | ||
function breakOut(child, after) { | ||
let prev = child.prev() | ||
after.after(child) | ||
while (prev && prev.type === 'comment') { | ||
let nextPrev = prev.prev() | ||
after.after(prev) | ||
prev = nextPrev | ||
} | ||
return child | ||
} | ||
function createFnAtruleChilds (bubble) { | ||
return function atruleChilds (rule, atrule, bubbling) { | ||
function createFnAtruleChilds(bubble) { | ||
return function atruleChilds(rule, atrule, bubbling, mergeSels = bubbling) { | ||
let children = [] | ||
atrule.each(child => { | ||
if (child.type === 'comment') { | ||
children.push(child) | ||
} else if (child.type === 'decl') { | ||
children.push(child) | ||
} else if (child.type === 'rule' && bubbling) { | ||
child.selectors = selectors(rule, child) | ||
} else if (child.type === 'atrule') { | ||
if (child.nodes && bubble[child.name]) { | ||
atruleChilds(rule, child, true) | ||
} else { | ||
if (child.type === 'rule' && bubbling) { | ||
if (mergeSels) { | ||
child.selectors = mergeSelectors(rule, child) | ||
} | ||
} else if (child.type === 'atrule' && child.nodes) { | ||
if (bubble[child.name]) { | ||
atruleChilds(rule, child, mergeSels) | ||
} else if (atrule[rootRuleMergeSel] !== false) { | ||
children.push(child) | ||
} | ||
} else { | ||
children.push(child) | ||
} | ||
@@ -101,3 +121,3 @@ }) | ||
function pickDeclarations (selector, declarations, after, Rule) { | ||
function pickDeclarations(selector, declarations, after) { | ||
let parent = new Rule({ | ||
@@ -107,7 +127,3 @@ selector, | ||
}) | ||
for (let declaration of declarations) { | ||
parent.append(declaration) | ||
} | ||
parent.append(declarations) | ||
after.after(parent) | ||
@@ -117,11 +133,10 @@ return parent | ||
function atruleNames (defaults, custom) { | ||
function atruleNames(defaults, custom) { | ||
let list = {} | ||
for (let i of defaults) { | ||
list[i] = true | ||
for (let name of defaults) { | ||
list[name] = true | ||
} | ||
if (custom) { | ||
for (let i of custom) { | ||
let name = i.replace(/^@/, '') | ||
list[name] = true | ||
for (let name of custom) { | ||
list[name.replace(/^@/, '')] = true | ||
} | ||
@@ -132,4 +147,132 @@ } | ||
function parseRootRuleParams(params) { | ||
params = params.trim() | ||
let braceBlock = params.match(/^\((.*)\)$/) | ||
if (!braceBlock) { | ||
return { type: 'basic', selector: params } | ||
} | ||
let bits = braceBlock[1].match(/^(with(?:out)?):(.+)$/) | ||
if (bits) { | ||
let allowlist = bits[1] === 'with' | ||
let rules = Object.fromEntries( | ||
bits[2] | ||
.trim() | ||
.split(/\s+/) | ||
.map(name => [name, true]) | ||
) | ||
if (allowlist && rules.all) { | ||
return { type: 'noop' } | ||
} | ||
let escapes = rule => !!rules[rule] | ||
if (rules.all) { | ||
escapes = () => true | ||
} else if (allowlist) { | ||
escapes = rule => (rule === 'all' ? false : !rules[rule]) | ||
} | ||
return { | ||
type: 'withrules', | ||
escapes | ||
} | ||
} | ||
// Unrecognized brace block | ||
return { type: 'unknown' } | ||
} | ||
function getAncestorRules(leaf) { | ||
let lineage = [] | ||
let parent = leaf.parent | ||
while (parent && parent instanceof AtRule) { | ||
lineage.push(parent) | ||
parent = parent.parent | ||
} | ||
return lineage | ||
} | ||
function unwrapRootRule(rule) { | ||
let escapes = rule[rootRuleEscapes] | ||
if (!escapes) { | ||
rule.after(rule.nodes) | ||
} else { | ||
let nodes = rule.nodes | ||
let topEscaped | ||
let topEscapedIdx = -1 | ||
let breakoutLeaf | ||
let breakoutRoot | ||
let clone | ||
let lineage = getAncestorRules(rule) | ||
lineage.forEach((parent, i) => { | ||
if (escapes(parent.name)) { | ||
topEscaped = parent | ||
topEscapedIdx = i | ||
breakoutRoot = clone | ||
} else { | ||
let oldClone = clone | ||
clone = parent.clone({ nodes: [] }) | ||
oldClone && clone.append(oldClone) | ||
breakoutLeaf = breakoutLeaf || clone | ||
} | ||
}) | ||
if (!topEscaped) { | ||
rule.after(nodes) | ||
} else if (!breakoutRoot) { | ||
topEscaped.after(nodes) | ||
} else { | ||
let leaf = breakoutLeaf | ||
leaf.append(nodes) | ||
topEscaped.after(breakoutRoot) | ||
} | ||
if (rule.next() && topEscaped) { | ||
let restRoot | ||
lineage.slice(0, topEscapedIdx + 1).forEach((parent, i, arr) => { | ||
let oldRoot = restRoot | ||
restRoot = parent.clone({ nodes: [] }) | ||
oldRoot && restRoot.append(oldRoot) | ||
let nextSibs = [] | ||
let _child = arr[i - 1] || rule | ||
let next = _child.next() | ||
while (next) { | ||
nextSibs.push(next) | ||
next = next.next() | ||
} | ||
restRoot.append(nextSibs) | ||
}) | ||
restRoot && (breakoutRoot || nodes[nodes.length - 1]).after(restRoot) | ||
} | ||
} | ||
rule.remove() | ||
} | ||
const rootRuleMergeSel = Symbol('rootRuleMergeSel') | ||
const rootRuleEscapes = Symbol('rootRuleEscapes') | ||
function normalizeRootRule(rule) { | ||
let { params } = rule | ||
let { type, selector, escapes } = parseRootRuleParams(params) | ||
if (type === 'unknown') { | ||
throw rule.error( | ||
`Unknown @${rule.name} parameter ${JSON.stringify(params)}` | ||
) | ||
} | ||
if (type === 'basic' && selector) { | ||
let selectorBlock = new Rule({ selector, nodes: rule.nodes }) | ||
rule.removeAll() | ||
rule.append(selectorBlock) | ||
} | ||
rule[rootRuleEscapes] = escapes | ||
rule[rootRuleMergeSel] = escapes ? !escapes('all') : type === 'noop' | ||
} | ||
const hasRootRule = Symbol('hasRootRule') | ||
module.exports = (opts = {}) => { | ||
let bubble = atruleNames(['media', 'supports'], opts.bubble) | ||
let bubble = atruleNames(['media', 'supports', 'layer'], opts.bubble) | ||
let atruleChilds = createFnAtruleChilds(bubble) | ||
@@ -146,2 +289,3 @@ let unwrap = atruleNames( | ||
) | ||
let rootRuleName = (opts.rootRuleName || 'at-root').replace(/^@/, '') | ||
let preserveEmpty = opts.preserveEmpty | ||
@@ -151,3 +295,11 @@ | ||
postcssPlugin: 'postcss-nested', | ||
Rule (rule, { Rule }) { | ||
Once(root) { | ||
root.walkAtRules(rootRuleName, node => { | ||
normalizeRootRule(node) | ||
root[hasRootRule] = true | ||
}) | ||
}, | ||
Rule(rule) { | ||
let unwrapped = false | ||
@@ -161,3 +313,3 @@ let after = rule | ||
if (declarations.length) { | ||
after = pickDeclarations(rule.selector, declarations, after, Rule) | ||
after = pickDeclarations(rule.selector, declarations, after) | ||
declarations = [] | ||
@@ -168,24 +320,13 @@ } | ||
unwrapped = true | ||
child.selectors = selectors(rule, child) | ||
after = pickComment(child.prev(), after) | ||
after.after(child) | ||
after = child | ||
child.selectors = mergeSelectors(rule, child) | ||
after = breakOut(child, after) | ||
} else if (child.type === 'atrule') { | ||
if (declarations.length) { | ||
after = pickDeclarations(rule.selector, declarations, after, Rule) | ||
after = pickDeclarations(rule.selector, declarations, after) | ||
declarations = [] | ||
} | ||
if (child.name === 'at-root') { | ||
if (child.name === rootRuleName) { | ||
unwrapped = true | ||
atruleChilds(rule, child, false) | ||
let nodes = child.nodes | ||
if (child.params) { | ||
nodes = new Rule({ selector: child.params, nodes }) | ||
} | ||
after.after(nodes) | ||
after = nodes | ||
child.remove() | ||
atruleChilds(rule, child, true, child[rootRuleMergeSel]) | ||
after = breakOut(child, after) | ||
} else if (bubble[child.name]) { | ||
@@ -195,5 +336,3 @@ copyDeclarations = true | ||
atruleChilds(rule, child, true) | ||
after = pickComment(child.prev(), after) | ||
after.after(child) | ||
after = child | ||
after = breakOut(child, after) | ||
} else if (unwrap[child.name]) { | ||
@@ -203,5 +342,3 @@ copyDeclarations = true | ||
atruleChilds(rule, child, false) | ||
after = pickComment(child.prev(), after) | ||
after.after(child) | ||
after = child | ||
after = breakOut(child, after) | ||
} else if (copyDeclarations) { | ||
@@ -216,3 +353,3 @@ declarations.push(child) | ||
if (declarations.length) { | ||
after = pickDeclarations(rule.selector, declarations, after, Rule) | ||
after = pickDeclarations(rule.selector, declarations, after) | ||
} | ||
@@ -224,2 +361,9 @@ | ||
} | ||
}, | ||
RootExit(root) { | ||
if (root[hasRootRule]) { | ||
root.walkAtRules(rootRuleName, unwrapRootRule) | ||
root[hasRootRule] = false | ||
} | ||
} | ||
@@ -226,0 +370,0 @@ } |
{ | ||
"name": "postcss-nested", | ||
"version": "5.0.6", | ||
"version": "6.0.0", | ||
"description": "PostCSS plugin to unwrap nested rules like how Sass does it", | ||
@@ -26,4 +26,4 @@ "keywords": [ | ||
"dependencies": { | ||
"postcss-selector-parser": "^6.0.6" | ||
"postcss-selector-parser": "^6.0.10" | ||
} | ||
} |
114
README.md
@@ -84,3 +84,3 @@ # PostCSS Nested | ||
[`postcss-nested-props`]: https://github.com/jedmao/postcss-nested-props | ||
[`postcss-nesting`]: https://github.com/jonathantneal/postcss-nesting | ||
[`postcss-nesting`]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting | ||
[CSSWG draft]: https://drafts.csswg.org/css-nesting-1/ | ||
@@ -90,111 +90,3 @@ [PostCSS]: https://github.com/postcss/postcss | ||
## Usage | ||
**Step 1:** Install plugin: | ||
```sh | ||
npm install --save-dev postcss postcss-nested | ||
``` | ||
**Step 2:** Check your project for existing PostCSS config: `postcss.config.js` | ||
in the project root, `"postcss"` section in `package.json` | ||
or `postcss` in bundle config. | ||
If you do not use PostCSS, add it according to [official docs] | ||
and set this plugin in settings. | ||
**Step 3:** Add the plugin to plugins list: | ||
```diff | ||
module.exports = { | ||
plugins: [ | ||
+ require('postcss-nested'), | ||
require('autoprefixer') | ||
] | ||
} | ||
``` | ||
[official docs]: https://github.com/postcss/postcss#usage | ||
## Options | ||
### `bubble` | ||
By default, plugin will bubble only `@media` and `@supports` at-rules. | ||
You can add your custom at-rules to this list by `bubble` option: | ||
```js | ||
postcss([ require('postcss-nested')({ bubble: ['phone'] }) ]) | ||
``` | ||
```css | ||
/* input */ | ||
a { | ||
color: white; | ||
@phone { | ||
color: black; | ||
} | ||
} | ||
/* output */ | ||
a { | ||
color: white; | ||
} | ||
@phone { | ||
a { | ||
color: black; | ||
} | ||
} | ||
``` | ||
### `unwrap` | ||
By default, plugin will unwrap only `@font-face`, `@keyframes` and `@document` | ||
at-rules. You can add your custom at-rules to this list by `unwrap` option: | ||
```js | ||
postcss([ require('postcss-nested')({ unwrap: ['phone'] }) ]) | ||
``` | ||
```css | ||
/* input */ | ||
a { | ||
color: white; | ||
@phone { | ||
color: black; | ||
} | ||
} | ||
/* output */ | ||
a { | ||
color: white; | ||
} | ||
@phone { | ||
color: black; | ||
} | ||
``` | ||
### `preserveEmpty` | ||
By default, plugin will strip out any empty selector generated by intermediate | ||
nesting levels. You can set `preserveEmpty` to `true` to preserve them. | ||
```css | ||
.a { | ||
.b { | ||
color: black; | ||
} | ||
} | ||
``` | ||
Will be compiled to: | ||
```css | ||
.a { } | ||
.a .b { | ||
color: black; | ||
} | ||
``` | ||
This is especially useful if you want to export the empty classes with `postcss-modules`. | ||
## Docs | ||
Read **[full docs](https://github.com/postcss/postcss-nested#readme)** on GitHub. |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
14122
359
91
1