diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index acbf8a5..0000000 --- a/.appveyor.yml +++ /dev/null @@ -1,18 +0,0 @@ -# https://www.appveyor.com/docs/appveyor-yml - -environment: - matrix: - - nodejs_version: 4 - -version: "{build}" -build: off -deploy: off - -install: - - ps: Install-Product node $env:nodejs_version - - npm install --ignore-scripts - -test_script: - - node --version - - npm --version - - cmd: "npm test" diff --git a/.gitignore b/.gitignore index de73e5a..688f9cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,11 @@ -node_modules -index.*.js -package-lock.json -*.log* -*.result.css .* -!.appveyor.yml !.editorconfig !.gitignore !.rollup.js !.tape.js !.travis.yml +*.log* +*.result.css +/index.* +node_modules +package-lock.json diff --git a/.rollup.js b/.rollup.js index 0436758..1a4c229 100644 --- a/.rollup.js +++ b/.rollup.js @@ -1,15 +1,15 @@ import babel from 'rollup-plugin-babel'; export default { - input: 'index.js', + input: 'src/index.js', output: [ - { file: 'index.cjs.js', format: 'cjs' }, - { file: 'index.es.js', format: 'es' } + { file: 'index.js', format: 'cjs', sourcemap: true, strict: false }, + { file: 'index.mjs', format: 'esm', sourcemap: true, strict: false } ], plugins: [ babel({ presets: [ - ['env', { modules: false, targets: { node: 4 } }] + ['@babel/env', { modules: false, targets: { node: 8 } }] ] }) ] diff --git a/.tape.js b/.tape.js index a4ef6a3..1ca6163 100644 --- a/.tape.js +++ b/.tape.js @@ -1,5 +1,8 @@ +const postcss = require('postcss'); +const postcssNesting = require('postcss-nesting'); +const postcssExtends = require('.'); + module.exports = { - 'postcss-extend-rule': { 'basic': { message: 'supports @extend usage' }, @@ -23,32 +26,47 @@ module.exports = { }, 'advanced': { message: 'supports mixed usage (with postcss-nesting)', - plugin: () => require('postcss')( - require('postcss-nesting'), - require('.') - ) + plugin: postcss.plugin('postcss-extend-rule', () => { + const extendsTransformer = postcssExtends(); + const nestingTransformer = postcssNesting(); + + return (...args) => { + nestingTransformer(...args); + extendsTransformer(...args); + }; + }) }, 'nested-media': { 'message': 'supports nested @media usage' }, 'nested-media:nesting-first': { 'message': 'supports nested @media usage when postcss-nesting runs first', - plugin: () => require('postcss')( - require('postcss-nesting'), - require('.') - ) + plugin: postcss.plugin('postcss-extend-rule', () => { + const extendsTransformer = postcssExtends(); + const nestingTransformer = postcssNesting(); + + return (...args) => { + nestingTransformer(...args); + extendsTransformer(...args); + }; + }) }, 'nested-media:nesting-second': { 'message': 'supports nested @medi usage when postcss-nesting runs second', - plugin: () => require('postcss')( - require('.'), - require('postcss-nesting') - ) + plugin: postcss.plugin('postcss-extend-rule', () => { + const extendsTransformer = postcssExtends(); + const nestingTransformer = postcssNesting(); + + return (...args) => { + extendsTransformer(...args); + nestingTransformer(...args); + }; + }) }, - 'errors': { + 'error': { message: 'manages error-ridden usage' }, - 'errors:ignore': { + 'error:ignore': { message: 'manages error-ridden usage with { onFunctionalSelector: "ignore", onRecursiveExtend: "ignore", onUnusedExtend: "ignore" } options', options: { onFunctionalSelector: 'ignore', @@ -56,16 +74,16 @@ module.exports = { onUnusedExtend: 'ignore' } }, - 'errors:warn': { + 'error:warn': { message: 'manages error-ridden usage with { onFunctionalSelector: "warn", onRecursiveExtend: "warn", onUnusedExtend: "warn" } options', options: { onFunctionalSelector: 'warn', onRecursiveExtend: 'warn', onUnusedExtend: 'warn' }, - warning: 6 + warnings: 2 }, - 'errors:throw': { + 'error:throw': { message: 'manages error-ridden usage with { onFunctionalSelector: "throw", onRecursiveExtend: "throw", onUnusedExtend: "throw" } options', options: { onFunctionalSelector: 'throw', @@ -76,7 +94,7 @@ module.exports = { reason: 'Unused extend at-rule "some-non-existent-selector"' } }, - 'errors:throw-on-functional-selectors': { + 'error:throw-on-functional-selectors': { message: 'manages error-ridden usage with { onFunctionalSelector: "throw" } options', options: { onFunctionalSelector: 'throw' @@ -85,5 +103,4 @@ module.exports = { reason: 'Encountered functional selector "%test-placeholder"' } } - } }; diff --git a/.travis.yml b/.travis.yml index c564664..6ebed1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: node_js node_js: - - 4 + - 8 install: - npm install --ignore-scripts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00eda4b..7fd7a32 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ scope and avoid unrelated commits. cd postcss-extend-rule # Assign the original repo to a remote called "upstream" - git remote add upstream git@github.com:jonathantneal/postcss-extend-rule.git + git remote add upstream git@github.com:csstools/postcss-extend-rule.git # Install the tools necessary for testing npm install diff --git a/README.md b/README.md index 3b1be1c..e836297 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PostCSS Extend Rule [PostCSS Logo][postcss] +# PostCSS Extend Rule [PostCSS][postcss] [![NPM Version][npm-img]][npm-url] [![Build Status][cli-img]][cli-url] @@ -57,126 +57,35 @@ ## Usage -Add [PostCSS Extend Rule] to your build tool: +Add [PostCSS Extend Rule] to your project: ```bash npm install postcss-extend-rule --save-dev ``` -#### Node - -Use [PostCSS Extend Rule] to process your CSS: +Use **PostCSS Extend Rule** to process your CSS: ```js -import postcssExtend from 'postcss-extend-rule'; +const postcssExtendRule = require('postcss-extend-rule'); -postcssExtend.process(YOUR_CSS, /* processOptions */ /*, pluginOptions */); +postcssExtendRule.process(YOUR_CSS /*, processOptions, pluginOptions */); ``` -#### PostCSS - -Add [PostCSS] to your build tool: - -```bash -npm install postcss --save-dev -``` - -Use [PostCSS Extend Rule] as a plugin: +Or use it as a [PostCSS] plugin: ```js -import postcss from 'gulp-postcss'; -import postcssExtend from 'postcss-extend-rule'; +const postcss = require('postcss'); +const postcssExtendRule = require('postcss-extend-rule'); postcss([ - postcssExtend(/* pluginOptions */) -]).process(YOUR_CSS); -``` - -#### Webpack - -Add [PostCSS Loader] to your build tool: - -```bash -npm install postcss-loader --save-dev -``` - -Use [PostCSS Extend Rule] in your Webpack configuration: - -```js -import postcssExtend from 'postcss-extend-rule'; - -module.exports = { - module: { - rules: [ - { - test: /\.css$/, - use: [ - 'style-loader', - { loader: 'css-loader', options: { importLoaders: 1 } }, - { loader: 'postcss-loader', options: { - ident: 'postcss', - plugins: () => [ - postcssExtend(/* pluginOptions */) - ] - } } - ] - } - ] - } -} + postcssExtendRule(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); ``` -#### Gulp +**PostCSS Extend Rule** runs in all Node environments, with special instructions for: -Add [Gulp PostCSS] to your build tool: - -```bash -npm install gulp-postcss --save-dev -``` - -Use [PostCSS Extend Rule] in your Gulpfile: - -```js -import postcss from 'gulp-postcss'; -import postcssExtend from 'postcss-extend-rule'; - -gulp.task('css', () => gulp.src('./src/*.css').pipe( - postcss([ - postcssExtend(/* pluginOptions */) - ]) -).pipe( - gulp.dest('.') -)); -``` - -#### Grunt - -Add [Grunt PostCSS] to your build tool: - -```bash -npm install grunt-postcss --save-dev -``` - -Use [PostCSS Extend Rule] in your Gruntfile: - -```js -import postcssExtend from 'postcss-extend-rule'; - -grunt.loadNpmTasks('grunt-postcss'); - -grunt.initConfig({ - postcss: { - options: { - use: [ - postcssExtend(/* pluginOptions */) - ] - }, - dist: { - src: '*.css' - } - } -}); -``` +| [Node](INSTALL.md#node) | [PostCSS CLI](INSTALL.md#postcss-cli) | [Webpack](INSTALL.md#webpack) | [Create React App](INSTALL.md#create-react-app) | [Gulp](INSTALL.md#gulp) | [Grunt](INSTALL.md#grunt) | +| --- | --- | --- | --- | --- | --- | ## Options @@ -256,8 +165,8 @@ main { } ``` -[cli-img]: https://img.shields.io/travis/jonathantneal/postcss-extend-rule.svg -[cli-url]: https://travis-ci.org/jonathantneal/postcss-extend-rule +[cli-img]: https://img.shields.io/travis/csstools/postcss-extend-rule/master.svg +[cli-url]: https://travis-ci.org/csstools/postcss-extend-rule [git-img]: https://img.shields.io/badge/support-chat-blue.svg [git-url]: https://gitter.im/postcss/postcss [npm-img]: https://img.shields.io/npm/v/postcss-extend-rule.svg @@ -265,8 +174,5 @@ main { [CSS Extend Rules Specification]: https://jonathantneal.github.io/specs/css-extend-rule/ [Functional Selectors]: https://jonathantneal.github.io/specs/css-extend-rule/#functional-selector -[Gulp PostCSS]: https://github.com/postcss/gulp-postcss -[Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss [PostCSS]: https://github.com/postcss/postcss -[PostCSS Loader]: https://github.com/postcss/postcss-loader -[PostCSS Extend Rule]: https://github.com/jonathantneal/postcss-extend-rule +[PostCSS Extend Rule]: https://github.com/csstools/postcss-extend-rule diff --git a/package.json b/package.json index 3ea5c2a..1fa5a30 100644 --- a/package.json +++ b/package.json @@ -1,46 +1,59 @@ { "name": "postcss-extend-rule", - "version": "2.0.0", + "version": "3.0.0", "description": "Use the @extend at-rule and functional selectors in CSS", "author": "Jonathan Neal ", "license": "CC0-1.0", - "repository": "jonathantneal/postcss-extend-rule", - "homepage": "https://github.com/jonathantneal/postcss-extend-rule#readme", - "bugs": "https://github.com/jonathantneal/postcss-extend-rule/issues", - "main": "index.cjs.js", - "module": "index.es.js", + "repository": "csstools/postcss-extend-rule", + "homepage": "https://github.com/csstools/postcss-extend-rule#readme", + "bugs": "https://github.com/csstools/postcss-extend-rule/issues", + "main": "index.js", + "module": "index.mjs", "files": [ - "index.cjs.js", - "index.es.js" + "index.js", + "index.js.map", + "index.mjs", + "index.mjs.map" ], "scripts": { + "build": "rollup --config .rollup.js --silent", "prepublishOnly": "npm test", - "pretest": "rollup -c .rollup.js --silent", - "test": "echo 'Running tests...'; npm run test:js && npm run test:tape", - "test:js": "eslint *.js --cache --ignore-path .gitignore --quiet", + "pretest:tape": "npm run build", + "test": "npm run test:js && npm run test:tape", + "test:js": "eslint src/{*,**/*}.js --cache --ignore-path .gitignore --quiet", "test:tape": "postcss-tape" }, "engines": { - "node": ">=4.0.0" + "node": ">=8.0.0" }, "dependencies": { - "postcss": "^6.0.22", - "postcss-nesting": "^5.0.0" + "postcss": "^7.0.17", + "postcss-nesting": "^7.0.1", + "postcss-tape": "^5.0.2" }, "devDependencies": { - "babel-core": "^6.26.3", - "babel-eslint": "^8.2.3", - "babel-preset-env": "^1.7.0", - "eslint": "^4.19.1", - "eslint-config-dev": "^2.0.0", - "postcss-tape": "^2.2.0", + "@babel/core": "^7.5.5", + "@babel/preset-env": "^7.5.5", + "babel-eslint": "^10.0.2", + "eslint": "^6.1.0", "pre-commit": "^1.2.2", - "rollup": "^0.59.4", - "rollup-plugin-babel": "^3.0.4" + "rollup": "^1.17.0", + "rollup-plugin-babel": "^4.3.3" }, "eslintConfig": { - "extends": "dev", - "parser": "babel-eslint" + "env": { + "browser": true, + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 2018, + "impliedStrict": true, + "sourceType": "module" + }, + "root": true }, "keywords": [ "postcss", diff --git a/index.js b/src/index.js similarity index 88% rename from index.js rename to src/index.js index f43a3d5..91139a3 100644 --- a/index.js +++ b/src/index.js @@ -1,5 +1,7 @@ import postcss from 'postcss'; -import nesting from 'postcss-nesting'; +import postcssNesting from 'postcss-nesting'; + +const nesting = postcssNesting(); // functional selector match const functionalSelectorMatch = /(^|[^\w-])(%[_a-zA-Z]+[_a-zA-Z0-9-]*)([^\w-]|$)/i; @@ -15,11 +17,19 @@ export default postcss.plugin('postcss-extend-rule', rawopts => { : 'extend'; return (root, result) => { + const extendedAtRules = new WeakMap(); + // for each extend at-rule root.walkAtRules(extendMatch, extendAtRule => { + let parent = extendAtRule.parent; + + while (parent.parent && parent.parent !== root) { + parent = parent.parent; + } + // do not revisit visited extend at-rules - if (!extendAtRule.__extendAtRuleVisited) { - extendAtRule.__extendAtRuleVisited = true; + if (!extendedAtRules.has(extendAtRule)) { + extendedAtRules.set(extendAtRule, true); // selector identifier const selectorIdMatch = getSelectorIdMatch(extendAtRule.params); @@ -33,13 +43,11 @@ export default postcss.plugin('postcss-extend-rule', rawopts => { extendAtRule.replaceWith(extendingRules); // transform any nesting at-rules - extendingRules.forEach( - extendingRule => { - transform(extendingRule); + const cloneRoot = postcss.root().append(parent.clone()); - extendingRule.walk(transform); - } - ); + nesting(cloneRoot); + + parent.replaceWith(cloneRoot); } else { // manage unused extend at-rules const unusedExtendMessage = `Unused extend at-rule "${extendAtRule.params}"`; @@ -81,14 +89,6 @@ export default postcss.plugin('postcss-extend-rule', rawopts => { }; }); -function transform(node) { - return nesting()({ - walk(transformer) { - return transformer(node) - } - }); -} - function getExtendingRules(selectorIdMatch, extendAtRule) { // extending rules const extendingRules = []; @@ -138,7 +138,7 @@ function getSelectorIdMatch(selectorIds) { ).join('|'); // selector unattached to an existing selector - const selectorIdMatch = new RegExp(`(^|[^\\w-]!\.!\#)(${escapedSelectorIds})([^\\w-]|$)`, ''); + const selectorIdMatch = new RegExp(`(^|[^\\w-]!.!#)(${escapedSelectorIds})([^\\w-]|$)`, ''); return selectorIdMatch; } diff --git a/test/advanced.expect.css b/test/advanced.expect.css index db86fca..038cdea 100644 --- a/test/advanced.expect.css +++ b/test/advanced.expect.css @@ -1,20 +1,24 @@ .serious-modal { font-style: normal; - font-weight: bold; + font-weight: bold } @media (max-width: 240px) { - .serious-modal:not(:focus) { - outline: none; +.serious-modal:not(:focus) { + outline: none +} } + +.modal { + border: thick dotted red } .modal { - border: thick dotted red; - color: red; + + color: red } .modal:hover:not(:focus) { - outline: none; -} + outline: none; + } diff --git a/test/basic.button.expect.css b/test/basic.button.expect.css index 60991f0..0042fb6 100644 --- a/test/basic.button.expect.css +++ b/test/basic.button.expect.css @@ -4,10 +4,16 @@ button { .button { color: red; +} + +.button { background: blue; } #button { color: red; +} + +#button { background: lime; } diff --git a/test/basic.expect.css b/test/basic.expect.css index f85069a..73a23cd 100644 --- a/test/basic.expect.css +++ b/test/basic.expect.css @@ -17,5 +17,6 @@ } .serious-modal { + font-weight: bold; } diff --git a/test/errors.css b/test/error.css similarity index 100% rename from test/errors.css rename to test/error.css diff --git a/test/errors.expect.css b/test/error.expect.css similarity index 55% rename from test/errors.expect.css rename to test/error.expect.css index 86b6157..751a6c5 100644 --- a/test/errors.expect.css +++ b/test/error.expect.css @@ -2,10 +2,13 @@ test-does-not-extend-non-existent-selector { } test-does-not-extend-itself { + @extend test-does-not-extend-itself; } test-does-not-extend-itself-cleverly-1 { + @extend test-does-not-extend-itself-cleverly-1; } test-does-not-extend-itself-cleverly-2 { + @extend test-does-not-extend-itself-cleverly-1; } diff --git a/test/errors.ignore.expect.css b/test/error.ignore.expect.css similarity index 58% rename from test/errors.ignore.expect.css rename to test/error.ignore.expect.css index c137b53..9cfd9f9 100644 --- a/test/errors.ignore.expect.css +++ b/test/error.ignore.expect.css @@ -3,21 +3,17 @@ test-does-not-extend-non-existent-selector { } test-does-not-extend-itself { - - @extend test-does-not-extend-itself + @extend test-does-not-extend-itself; } test-does-not-extend-itself-cleverly-1 { - - @extend test-does-not-extend-itself-cleverly-1 + @extend test-does-not-extend-itself-cleverly-1; } test-does-not-extend-itself-cleverly-2 { - - @extend test-does-not-extend-itself-cleverly-1 + @extend test-does-not-extend-itself-cleverly-1; } %test-placeholder { - - @extend %test-placeholder + @extend %test-placeholder; } diff --git a/test/errors.warn.expect.css b/test/error.warn.expect.css similarity index 58% rename from test/errors.warn.expect.css rename to test/error.warn.expect.css index c137b53..9cfd9f9 100644 --- a/test/errors.warn.expect.css +++ b/test/error.warn.expect.css @@ -3,21 +3,17 @@ test-does-not-extend-non-existent-selector { } test-does-not-extend-itself { - - @extend test-does-not-extend-itself + @extend test-does-not-extend-itself; } test-does-not-extend-itself-cleverly-1 { - - @extend test-does-not-extend-itself-cleverly-1 + @extend test-does-not-extend-itself-cleverly-1; } test-does-not-extend-itself-cleverly-2 { - - @extend test-does-not-extend-itself-cleverly-1 + @extend test-does-not-extend-itself-cleverly-1; } %test-placeholder { - - @extend %test-placeholder + @extend %test-placeholder; } diff --git a/test/nested-media.expect.css b/test/nested-media.expect.css index 4ddf203..7096b05 100644 --- a/test/nested-media.expect.css +++ b/test/nested-media.expect.css @@ -8,8 +8,11 @@ .test-content { color: red; +} - @media screen and (min-width: 920px) { +@media screen and (min-width: 920px) { + + .test-content { color: green; - } } + } diff --git a/test/nested-media.nesting-first.expect.css b/test/nested-media.nesting-first.expect.css index bb6f336..a82c9b2 100644 --- a/test/nested-media.nesting-first.expect.css +++ b/test/nested-media.nesting-first.expect.css @@ -1,13 +1,13 @@ .my_placeholder { - color: red; + color: red } @media screen and (min-width: 920px) { - .my_placeholder { - color: green; - } +.my_placeholder { + color: green } + } .test-content { color: red; @@ -17,5 +17,5 @@ .test-content { color: green; - } } + } diff --git a/test/nested-media.nesting-second.expect.css b/test/nested-media.nesting-second.expect.css index bb6f336..a82c9b2 100644 --- a/test/nested-media.nesting-second.expect.css +++ b/test/nested-media.nesting-second.expect.css @@ -1,13 +1,13 @@ .my_placeholder { - color: red; + color: red } @media screen and (min-width: 920px) { - .my_placeholder { - color: green; - } +.my_placeholder { + color: green } + } .test-content { color: red; @@ -17,5 +17,5 @@ .test-content { color: green; - } } + }