diff --git a/CHANGELOG.md b/CHANGELOG.md index bb83431..9709656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Change Log This project adheres to [Semantic Versioning](http://semver.org/). +## Upcoming... +* ... +* fix: Correctly handle `with`/`without` parameters on `@at-root` +* feat: Add option `rootRuleName` to rename the custom `@at-root` rule +* fix: Errors when handling sibling `@at-root` rule blocks +* fix: Move all preceeding comments with rule +* fix: `@layer` blocks should also bubble + ## 5.0.6 * Fixed custom at-rules nesting (by @bsak-shell). diff --git a/README.md b/README.md index fd71ac6..e0282c7 100644 --- a/README.md +++ b/README.md @@ -119,8 +119,8 @@ module.exports = { ### `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: +By default, plugin will bubble only `@media`, `@supports` and `@layer` +at-rules. Use this option to add your custom at-rules to this list. ```js postcss([ require('postcss-nested')({ bubble: ['phone'] }) ]) @@ -196,3 +196,27 @@ Will be compiled to: ``` This is especially useful if you want to export the empty classes with `postcss-modules`. + + +### `rootRuleName` + +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. + +```js +postcss([ require('postcss-nested')({ rootRuleName: '_escape-nesting' }) ]) +``` + +```css +/* input */ +.a { + color: white; + @_escape-nesting { + .b { color: black; } + } +} +/* output */ +.a { color: white; } +.b { color: black; } +``` diff --git a/index.d.ts b/index.d.ts index d22bcdd..5367682 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,8 +6,8 @@ import { PluginCreator } from 'postcss' declare namespace nested { interface Options { /** - * 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. */ bubble?: string[] @@ -24,6 +24,13 @@ declare namespace nested { * to preserve them. */ 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 } type Nested = PluginCreator diff --git a/index.js b/index.js index e6ebd92..6b93bf4 100644 --- a/index.js +++ b/index.js @@ -1,87 +1,152 @@ +// @ts-check +const { Rule, AtRule } = require('postcss') let parser = require('postcss-selector-parser') -function parse (str, rule) { +/** @typedef {import('postcss').Container} Container */ +/** @typedef {import('postcss').ChildNode} ChildNode */ +/** @typedef {import('postcss').Comment} Comment */ +/** @typedef {import('postcss').Declaration} Declaration */ +/** @typedef {import('postcss').Rule} PostcssRule */ +/** @typedef {typeof import('postcss').Rule} RuleConstructor */ +/** @typedef {parser.Root} Root */ +/** @typedef {parser.Node} Node */ +/** @typedef {parser.Selector} Selector */ +/** @typedef {Record} RuleMap Simple lookup table for \@-rules */ + +/** + * Run a selector string through postcss-selector-parser + * + * @param {string} rawSelector + * @param {PostcssRule} [rule] + * @returns {Selector} + */ +function parse(rawSelector, rule) { + /** @type {Root | undefined} */ 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 } else { throw rule ? rule.error(e.message) : e } } - return nodes.at(0) + // Should be safe, but @ts-check can't deduce the side-effect + // triggered by `saver.processSync(str)` + return /** @type {Root} */ (nodes).at(0) } -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 + * + * @param {Extract} nodes + * @param {Selector} parent + * @returns {boolean} Indicating whether a replacement took place or not. + */ +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()))) - } else { - i.replaceWith(clonedParent) - } - replaced = true - } else if (i.nodes) { - if (replace(i, parent)) { + nodes.each( + /** @type {Node} */ node => { + if (node.type === 'nesting') { + let clonedParent = parent.clone({}) + if (node.value !== '&') { + node.replaceWith( + parse(node.value.replace('&', clonedParent.toString())) + ) + } else { + node.replaceWith(clonedParent) + } replaced = true + } else if ('nodes' in node && node.nodes) { + if (interpolateAmpInSelector(node, parent)) { + replaced = true + } } } - }) + ) return replaced } -function selectors (parent, child) { - let result = [] - parent.selectors.forEach(i => { - let parentNode = parse(i, 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()) +/** + * Combines parent and child selectors, in a SCSS-like way + * + * @param {PostcssRule} parent + * @param {PostcssRule} child + * @returns {Array} An array of new, merged selectors + */ +function mergeSelectors(parent, child) { + /** @type {Array} */ + let merged = [] + parent.selectors.forEach(sel => { + let parentNode = parse(sel, parent) + + child.selectors.forEach(selector => { + if (!selector) { + return + } + let node = parse(selector, child) + let replaced = interpolateAmpInSelector(node, parentNode) + if (!replaced) { + // NOTE: The type definitions for `postcss-selector-parser` seem to be + // badly outdated. + node.prepend(/** @type {any} */ (parser.combinator({ value: ' ' }))) + node.prepend(/** @type {Selector} */ (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" + * + * @param {ChildNode} child + * @param {ChildNode} after + * @returns {ChildNode} updated "after" node + */ +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) { +/** + * @param {RuleMap} bubble + */ +function createFnAtruleChilds(bubble) { + /** + * @param {PostcssRule} rule + * @param {AtRule} atrule + * @param {boolean} bubbling + */ + return function atruleChilds(rule, atrule, bubbling, mergeSels = bubbling) { + /** @type {Array} */ 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) } }) if (bubbling) { @@ -96,36 +161,198 @@ function createFnAtruleChilds (bubble) { } } -function pickDeclarations (selector, declarations, after, Rule) { +/** + * @param {string} selector + * @param {Array} declarations + * @param {ChildNode} after + */ +function pickDeclarations(selector, declarations, after) { let parent = new Rule({ selector, nodes: [] }) - - for (let declaration of declarations) { - parent.append(declaration) - } - + parent.append(declarations) after.after(parent) return parent } -function atruleNames (defaults, custom) { +/** + * @param {Array} defaults, + * @param {Array} [custom] + */ +function atruleNames(defaults, custom) { + /** @type {RuleMap} */ 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 } } return list } +/** @typedef {{ type: 'basic', selector?: string, escapes?: never }} AtRootBParams */ +/** @typedef {{ type: 'withrules', escapes: (rule: string) => boolean, selector?: never }} AtRootWParams */ +/** @typedef {{ type: 'unknown', selector?: never, escapes?: never }} AtRootUParams */ +/** @typedef {{ type: 'noop', selector?: never, escapes?: never }} AtRootNParams */ +/** @typedef {AtRootBParams | AtRootWParams | AtRootNParams | AtRootUParams} AtRootParams */ + +/** @type {(params: string) => AtRootParams } */ +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' + /** @type {RuleMap} */ + 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' } +} + +/** + * @param {AtRule} leaf + * @returns {Array} + */ +function getAncestorRules(leaf) { + /** @type {Array} */ + const lineage = [] + /** @type {Container | ChildNode | Document | undefined} */ + let parent = leaf.parent + + while (parent && parent instanceof AtRule) { + lineage.push(/** @type {AtRule} */ (parent)) + parent = parent.parent + } + return lineage +} + +/** + * @param {AtRule} rule + */ +function unwrapRootRule(rule) { + const escapes = rule[rootRuleEscapes] + + if (!escapes) { + rule.after(rule.nodes) + } else { + const nodes = rule.nodes + + /** @type {AtRule | undefined} */ + let topEscaped + let topEscapedIdx = -1 + /** @type {AtRule | undefined} */ + let breakoutLeaf + /** @type {AtRule | undefined} */ + let breakoutRoot + /** @type {AtRule | undefined} */ + let clone + + const lineage = getAncestorRules(rule) + lineage.forEach((parent, i) => { + if (escapes(parent.name)) { + topEscaped = parent + topEscapedIdx = i + breakoutRoot = clone + } else { + const 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 { + const leaf = /** @type {AtRule} */ (breakoutLeaf) + leaf.append(nodes) + topEscaped.after(breakoutRoot) + } + + if (rule.next() && topEscaped) { + /** @type {AtRule | undefined} */ + let restRoot + lineage.slice(0, topEscapedIdx + 1).forEach((parent, i, arr) => { + const oldRoot = restRoot + restRoot = parent.clone({ nodes: [] }) + oldRoot && restRoot.append(oldRoot) + + /** @type {Array} */ + 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') + +/** + * @param {AtRule} rule + */ +function normalizeRootRule(rule) { + let { params } = rule + const { 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') + +// --------------------------------------------------------------------------- + +/** @type {import('./').Nested} */ module.exports = (opts = {}) => { - let bubble = atruleNames(['media', 'supports'], opts.bubble) + let bubble = atruleNames(['media', 'supports', 'layer'], opts.bubble) let atruleChilds = createFnAtruleChilds(bubble) let unwrap = atruleNames( [ @@ -137,61 +364,57 @@ module.exports = (opts = {}) => { ], opts.unwrap ) + let rootRuleName = (opts.rootRuleName || 'at-root').replace(/^@/, '') let preserveEmpty = opts.preserveEmpty return { postcssPlugin: 'postcss-nested', - Rule (rule, { Rule }) { + + Once(root) { + root.walkAtRules(rootRuleName, node => { + normalizeRootRule(node) + root[hasRootRule] = true + }) + }, + + Rule(rule) { let unwrapped = false + /** @type {ChildNode} */ let after = rule let copyDeclarations = false + /** @type {Array} */ let declarations = [] rule.each(child => { if (child.type === 'rule') { if (declarations.length) { - after = pickDeclarations(rule.selector, declarations, after, Rule) + after = pickDeclarations(rule.selector, declarations, after) declarations = [] } copyDeclarations = true 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]) { copyDeclarations = true unwrapped = true atruleChilds(rule, child, true) - after = pickComment(child.prev(), after) - after.after(child) - after = child + after = breakOut(child, after) } else if (unwrap[child.name]) { copyDeclarations = true unwrapped = true atruleChilds(rule, child, false) - after = pickComment(child.prev(), after) - after.after(child) - after = child + after = breakOut(child, after) } else if (copyDeclarations) { declarations.push(child) } @@ -201,13 +424,20 @@ module.exports = (opts = {}) => { }) if (declarations.length) { - after = pickDeclarations(rule.selector, declarations, after, Rule) + after = pickDeclarations(rule.selector, declarations, after) } if (unwrapped && preserveEmpty !== true) { rule.raws.semicolon = true if (rule.nodes.length === 0) rule.remove() } + }, + + RootExit(root) { + if (root[hasRootRule]) { + root.walkAtRules(rootRuleName, unwrapRootRule) + root[hasRootRule] = false + } } } } diff --git a/index.test.js b/index.test.js index e122a1b..bf4f6c1 100644 --- a/index.test.js +++ b/index.test.js @@ -1,12 +1,30 @@ +// @ts-check let { equal, throws } = require('uvu/assert') let { test } = require('uvu') -let postcss = require('postcss') +let postcss = require('postcss').default let plugin = require('./') +/** + * @param {string} css + * @returns {string} + */ +function normalise(css) { + return css + .replace(/([:;{}]|\*\/|\/\*)/g, ' $1 ') + .replace(/\s\s+/g, ' ') + .replace(/ ([;:])/g, '$1') + .trim() +} + +/** + * @param {string} input + * @param {string} output + * @param {plugin.Options | undefined} [opts] + */ function run(input, output, opts) { let result = postcss([plugin(opts)]).process(input, { from: '/test.css' }) - equal(result.css, output) + equal(normalise(result.css), normalise(output)) equal(result.warnings().length, 0) } @@ -31,10 +49,377 @@ test('hoists at-root', () => { run('a { & {} @at-root { b {} } }', 'a {} b {}') }) +test('hoists at-root 2', () => { + run('a { @at-root { b {} } }', 'b {}') +}) + test('at-root short hand', () => { run('a { & {} @at-root b { } }', 'a {} b {}') }) +test('hoists multiple at-roots', () => { + run( + `a { + b { + & {} + @at-root { + c1 {} + c2 {} + } + @at-root { + d {} + } + } + }`, + `a b {} + c1 {} + c2 {} + d {}` + ) +}) + +test('hoists at-root and media siblings', () => { + run( + `a { + x: x; + a2 {} + @at-root { + b {} + } + @media x { + c {} + } + /* asdadf */ + }`, + `a { + x: x; + /* asdadf */ + } + a a2 {} + b {} + @media x { + a c {} + }` + ) +}) + +test('at-root stops at media', () => { + run('@media x { a { & {} @at-root { b { } } } }', '@media x { a {} b {} }') +}) + +test('at-root unwraps nested media', () => { + run('a { & {} @media x { @at-root { b { } } } }', 'a {} @media x { b {} }') +}) + +test('nested at-root with nested media', () => { + run( + `a { + & {} + @at-root { + b { + @at-root { + c { + & {} + } + @media y { + d {} + } + } + } + } + }`, + `a {} + c {} + @media y { + d {} + }` + ) +}) + +test('tolerates immediately nested at-root', () => { + run( + `a { + & {} + @at-root { + @at-root foo { + c {} + } + } + }`, + `a {} + foo c {}` + ) +}) + +test('tolerates top-level at-root', () => { + run( + `@at-root { + a {} + } + @media x { + @at-root { + b {} + } + }`, + `a {} + @media x { + b {} + }` + ) +}) + +test('tolerates immediately nested at-root #2', () => { + run( + `@media x { + a { + & {} + @at-root { + @at-root (without: media) { + c {} + } + } + } + }`, + `@media x { + a {} + } + c {}` + ) +}) + +test('tolerates immediately nested at-root #3', () => { + run( + `@media x { + a { + & {} + @at-root (without: media) { + @at-root (without: media) { + c {} + } + } + } + }`, + `@media x { + a {} + } + a c {}` + ) +}) + +test('at-root supports (without: all)', () => { + run( + `@media x { + @supports (z:y) { + a { + & {} + @at-root (without: all) { + b {} + @media y { + c {} + } + } + b {} + } + } + }`, + `@media x { + @supports (z:y) { + a {} + } + } + b {} + @media y { + c {} + } + @media x { + @supports (z:y) { + a b {} + } + }` + ) +}) + +test('at-root supports (with: all)', () => { + run( + `@media x { + @supports (z:y) { + a { + & {} + @at-root (with: all) { + b {} + @media y { + c {} + } + @media z { + & {} + } + } + } + } + }`, + `@media x { + @supports (z:y) { + a {} + a b {} + @media y { + a c {} + } + @media z { + a {} + } + } + }` + ) +}) + +test('at-root supports (without: foo)', () => { + run( + `@media x { + a { + & {} + @at-root (without: media) { + b {} + } + } + }`, + `@media x { + a {} + } + a b {}` + ) +}) + +test('at-root supports (without: foo) 2', () => { + run( + `@supports (y:z) { + @media x { + a { + b {} + @at-root (without: media) { + c {} + } + } + } + }`, + `@supports (y:z) { + @media x { + a b {} + } + a c {} + }` + ) +}) + +test('at-root supports (with: foo)', () => { + run( + `@supports (y:z) { + @media x { + a { + b {} + @at-root (with: supports) { + c {} + } + } + } + }`, + `@supports (y:z) { + @media x { + a b {} + } + a c {} + }` + ) +}) + +test('at-root supports (without: foo) 3', () => { + run( + `@supports (y:z) { + @media x { + a { + b {} + @at-root (without: supports) { + c {} + } + } + } + }`, + `@supports (y:z) { + @media x { + a b {} + } + } + @media x { + a c {} + }` + ) +}) + +test('at-root supports (without: foo) 4', () => { + run( + `@media x { + @supports (y:z) { + a { + & {} + @at-root (without: supports) { + b {} + } + } + } + }`, + `@media x { + @supports (y:z) { + a {} + } + a b {} + }` + ) +}) + +test('at-root supports (without: foo) 5', () => { + run( + `@media x { + @supports (a:b) { + @media (y) { + @supports (c:d) { + a { + & {} + @at-root (without: supports) { + b {} + } + c {} + } + d {} + } + } + e {} + f {} + } + }`, + `@media x { + @supports (a:b) { + @media (y) { + @supports (c:d) { + a {} + } + } + } + @media (y) { + a b {} + } + @supports (a:b) { + @media (y) { + @supports (c:d) { + a c {} + d {} + } + } + e {} + f {} + } + }` + ) +}) + test('replaces ampersand', () => { run('a { body &:hover b {} }', 'body a:hover b {}') }) @@ -89,6 +474,89 @@ test('unwraps at-rules', () => { ) }) +test('leaves nested @media blocks as is', () => { + run( + `a { a: 1 } + a { + @media screen { + b { + @media (max-width: 100rem) { + @media (min-width: 50rem) { + a: 1 + } + } + } + } + }`, + `a { a: 1 } + @media screen { + @media (max-width: 100rem) { + @media (min-width: 50rem) { + a b { a: 1 } + } + } + }` + ) +}) + +test('@at-root fully espacpes nested @media blocks', () => { + run( + `a { x: 3 } + a { + @media screen { + b { + @media (max-width: 100rem) { + x: 2; + @at-root (without: media) { + @media (min-width: 50rem) { + x: 1; + } + } + } + } + } + }`, + `a { x: 3 } + @media screen { + @media (max-width: 100rem) { + a b { x: 2; } + } + } + @media (min-width: 50rem) { + a b { x: 1 } + }` + ) +}) + +test('Multi nested @media is resolved', () => { + run( + `a { + @media screen { + b { + @media (max-width: 100rem) { + y: y; + c { + @media (min-width: 50rem) { + x: x + } + } + } + } + } + }`, + `@media screen { + @media (max-width: 100rem) { + a b { + y: y + } + @media (min-width: 50rem) { + a b c { x:x } + } + } + }` + ) +}) + test('unwraps at-rules with interleaved properties', () => { run( 'a { a: 1 } a { color: red; @media screen { @supports (a: 1) { a: 1 } } background: green }', @@ -147,7 +615,7 @@ test('clears empty selector after comma', () => { }) test('moves comment with rule', () => { - run('a { /*B*/ b {} }', '/*B*/ a b {}') + run('a { /*B*/ /*B2*/ b {} }', '/*B*/ /*B2*/ a b {}') }) test('moves comment with at-rule', () => { @@ -226,7 +694,7 @@ test('works with other visitors', () => { } } } - mixinPlugin.postcss = true + mixinPlugin.postcss = /** @type {const} */ (true) let out = postcss([plugin, mixinPlugin]).process(css, { from: undefined }).css @@ -245,7 +713,7 @@ test('works with other visitors #2', () => { } } } - mixinPlugin.postcss = true + mixinPlugin.postcss = /** @type {const} */ (true) let out = postcss([plugin, mixinPlugin]).process(css, { from: undefined }).css @@ -266,6 +734,13 @@ test('shows clear errors on other errors', () => { }, ':2:3: Unexpected') }) +test('errors on unknown @at-root parameters', () => { + let css = 'a {\n @at-root (wonky: "blah") {\n b {}\n }\n}' + throws(() => { + css = postcss([plugin]).process(css, { from: undefined }).css + }, ':2:3: Unknown @at-root parameter "(wonky: \\"blah\\")"') +}) + test('third level dependencies', () => { run( '.text {&:hover{border-color: red;&:before{color: red;}}}', @@ -273,8 +748,41 @@ test('third level dependencies', () => { ) }) +test('bubbles @layer blocks', () => { + run( + `@media x { + a { + @layer foo { + x:x + } + } + }`, + `@media x { + @layer foo { + a { + x:x + } + } + }` + ) +}) + test('third level dependencies #2', () => { run('.selector{:global{h2{color:pink}}}', '.selector :global h2{color:pink}') }) +test('Name of at-root is configurable', () => { + const rootRuleName = '_foobar_' + run(`a { & {} @${rootRuleName} { b {} } }`, `a {} b {}`, { + rootRuleName + }) +}) + +test('The rooRuleName option may start with "@"', () => { + const rootRuleName = '@_foobar_' + run(`a { & {} ${rootRuleName} { b {} } }`, `a {} b {}`, { + rootRuleName + }) +}) + test.run() diff --git a/package.json b/package.json index 700e278..2e2a171 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,10 @@ "trailingComma": "none" }, "eslintConfig": { - "extends": "@logux/eslint-config" + "extends": "@logux/eslint-config", + "rules": { + "prefer-let/prefer-let": 0 + } }, "c8": { "exclude": [