Repository: postcss/postcss-custom-media Branch: master Commit: a9a0ce03d759 Files: 34 Total size: 46.7 KB Directory structure: gitextract_e30ojqmi/ ├── .editorconfig ├── .gitignore ├── .rollup.js ├── .tape.js ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── INSTALL.md ├── LICENSE.md ├── README.md ├── index.js ├── lib/ │ ├── custom-media-from-root.js │ ├── get-custom-media-from-imports.js │ ├── media-ast-from-string.js │ ├── transform-atrules.js │ ├── transform-media-list.js │ └── write-custom-media-to-exports.js ├── package.json └── test/ ├── basic.css ├── basic.expect.css ├── basic.import.expect.css ├── basic.preserve.expect.css ├── export-media.css ├── export-media.js ├── export-media.json ├── export-media.mjs ├── import-css.css ├── import-media.css ├── import-media.js ├── import-media.json ├── import.css ├── import.empty.expect.css ├── import.expect.css └── import.plugin.expect.css ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf indent_style = tab insert_final_newline = true trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [*.{json,md,yml}] indent_size = 2 indent_style = space ================================================ FILE: .gitignore ================================================ node_modules index.*.* package-lock.json *.log* *.result.css .* !.editorconfig !.gitignore !.rollup.js !.tape.js !.travis.yml ================================================ FILE: .rollup.js ================================================ import babel from '@rollup/plugin-babel'; export default { input: 'index.js', output: [ { file: 'index.cjs.js', format: 'cjs', sourcemap: true, exports: 'default' }, { file: 'index.es.mjs', format: 'es', sourcemap: true, exports: 'default' } ], plugins: [ babel({ babelHelpers: 'bundled', plugins: [ '@babel/plugin-syntax-dynamic-import' ], presets: [ ['@babel/env', { modules: false, targets: { node: 10 } }] ] }) ] }; ================================================ FILE: .tape.js ================================================ module.exports = { 'basic': { message: 'supports basic usage' }, 'basic:preserve': { message: 'supports { preserve: true } usage', options: { preserve: true } }, 'import': { message: 'supports { importFrom: { customMedia: { ... } } } usage', options: { importFrom: { customMedia: { '--mq-a': '(max-width: 30em), (max-height: 30em)', '--not-mq-a': 'not all and (--mq-a)' } } } }, 'import:import-fn': { message: 'supports { importFrom() } usage', options: { importFrom() { return { customMedia: { '--mq-a': '(max-width: 30em), (max-height: 30em)', '--not-mq-a': 'not all and (--mq-a)' } }; } }, expect: 'import.expect.css', result: 'import.result.css' }, 'import:import-fn-promise': { message: 'supports { async importFrom() } usage', options: { importFrom() { return new Promise(resolve => { resolve({ customMedia: { '--mq-a': '(max-width: 30em), (max-height: 30em)', '--not-mq-a': 'not all and (--mq-a)' } }) }); } }, expect: 'import.expect.css', result: 'import.result.css' }, 'import:json': { message: 'supports { importFrom: "test/import-media.json" } usage', options: { importFrom: 'test/import-media.json' }, expect: 'import.expect.css', result: 'import.result.css' }, 'import:js': { message: 'supports { importFrom: "test/import-media.js" } usage', options: { importFrom: 'test/import-media.js' }, expect: 'import.expect.css', result: 'import.result.css' }, 'import:css': { message: 'supports { importFrom: "test/import-media.css" } usage', options: { importFrom: 'test/import-media.css' }, expect: 'import.expect.css', result: 'import.result.css' }, 'import:css-from': { message: 'supports { importFrom: { from: "test/import-media.css" } } usage', options: { importFrom: { from: 'test/import-media.css' } }, expect: 'import.expect.css', result: 'import.result.css' }, 'import:css-from-type': { message: 'supports { importFrom: [ { from: "test/import-media.css", type: "css" } ] } usage', options: { importFrom: [ { from: 'test/import-media.css', type: 'css' } ] }, expect: 'import.expect.css', result: 'import.result.css' }, 'import:empty': { message: 'supports { importFrom: {} } usage', options: { importFrom: {} } }, 'basic:export': { message: 'supports { exportTo: { customMedia: { ... } } } usage', options: { exportTo: (global.__exportMediaObject = global.__exportMediaObject || { customMedia: null }) }, expect: 'basic.expect.css', result: 'basic.result.css', after() { if (__exportMediaObject.customMedia['--mq-a'] !== '(max-width: 30em), (max-height: 30em)') { throw new Error('The exportTo function failed'); } } }, 'basic:export-fn': { message: 'supports { exportTo() } usage', options: { exportTo(customMedia) { if (customMedia['--mq-a'] !== '(max-width: 30em), (max-height: 30em)') { throw new Error('The exportTo function failed'); } } }, expect: 'basic.expect.css', result: 'basic.result.css' }, 'basic:export-fn-promise': { message: 'supports { async exportTo() } usage', options: { exportTo(customMedia) { return new Promise((resolve, reject) => { if (customMedia['--mq-a'] !== '(max-width: 30em), (max-height: 30em)') { reject('The exportTo function failed'); } else { resolve(); } }); } }, expect: 'basic.expect.css', result: 'basic.result.css' }, 'basic:export-json': { message: 'supports { exportTo: "test/export-media.json" } usage', options: { exportTo: 'test/export-media.json' }, expect: 'basic.expect.css', result: 'basic.result.css', before() { global.__exportMediaString = require('fs').readFileSync('test/export-media.json', 'utf8'); }, after() { if (global.__exportMediaString !== require('fs').readFileSync('test/export-media.json', 'utf8')) { throw new Error('The original file did not match the freshly exported copy'); } } }, 'basic:export-js': { message: 'supports { exportTo: "test/export-media.js" } usage', options: { exportTo: 'test/export-media.js' }, expect: 'basic.expect.css', result: 'basic.result.css', before() { global.__exportMediaString = require('fs').readFileSync('test/export-media.js', 'utf8'); }, after() { if (global.__exportMediaString !== require('fs').readFileSync('test/export-media.js', 'utf8')) { throw new Error('The original file did not match the freshly exported copy'); } } }, 'basic:export-mjs': { message: 'supports { exportTo: "test/export-media.mjs" } usage', options: { exportTo: 'test/export-media.mjs' }, expect: 'basic.expect.css', result: 'basic.result.css', before() { global.__exportMediaString = require('fs').readFileSync('test/export-media.mjs', 'utf8'); }, after() { if (global.__exportMediaString !== require('fs').readFileSync('test/export-media.mjs', 'utf8')) { throw new Error('The original file did not match the freshly exported copy'); } } }, 'basic:export-css': { message: 'supports { exportTo: "test/export-media.css" } usage', options: { exportTo: 'test/export-media.css' }, expect: 'basic.expect.css', result: 'basic.result.css', before() { global.__exportMediaString = require('fs').readFileSync('test/export-media.css', 'utf8'); }, after() { if (global.__exportMediaString !== require('fs').readFileSync('test/export-media.css', 'utf8')) { throw new Error('The original file did not match the freshly exported copy'); } } }, 'basic:export-css-to': { message: 'supports { exportTo: { to: "test/export-media.css" } } usage', options: { exportTo: { to: 'test/export-media.css' } }, expect: 'basic.expect.css', result: 'basic.result.css', before() { global.__exportMediaString = require('fs').readFileSync('test/export-media.css', 'utf8'); }, after() { if (global.__exportMediaString !== require('fs').readFileSync('test/export-media.css', 'utf8')) { throw new Error('The original file did not match the freshly exported copy'); } } }, 'basic:export-css-to-type': { message: 'supports { exportTo: { to: "test/export-media.css", type: "css" } } usage', options: { exportTo: { to: 'test/export-media.css', type: 'css' } }, expect: 'basic.expect.css', result: 'basic.result.css', before() { global.__exportMediaString = require('fs').readFileSync('test/export-media.css', 'utf8'); }, after() { if (global.__exportMediaString !== require('fs').readFileSync('test/export-media.css', 'utf8')) { throw new Error('The original file did not match the freshly exported copy'); } } } }; ================================================ FILE: .travis.yml ================================================ # https://docs.travis-ci.com/user/travis-lint language: node_js node_js: - 14 - 12 - 10 install: - npm install --ignore-scripts ================================================ FILE: CHANGELOG.md ================================================ # Changes to PostCSS Custom Media ### 8.0.0 (January 12, 2021) - Added: Support for PostCSS v8 ### 7.0.8 (March 30, 2019) - Fixed: Issue importing from `.pcss` files - Updated: `postcss` to 7.0.14 (patch) ### 7.0.7 (October 19, 2018) - Fixed: Issue combining custom media media queries with `and` ### 7.0.6 (October 12, 2018) - Fixed: Issue combining multiple custom media ### 7.0.5 (October 5, 2018) - Fixed: Possible issues resolving paths to imports and exports - Added: Imports from `customMedia` and `custom-media` simultaneously - Updated: `postcss` to 7.0.5 ### 7.0.4 (September 23, 2018) - Added: `importFromPlugins` option to process imports ### 7.0.3 (September 20, 2018) - Fixed: Do not break on an empty `importFrom` object ### 7.0.2 (September 15, 2018) - Fixed: An issue with re-assigning params as a non-string ### 7.0.1 (September 14, 2018) - Fixed: An issue with how opposing queries are resolved. ### 7.0.0 (September 14, 2018) - Added: New `preserve` option to preserve custom media and atrules using them - Added: New `exportTo` function to specify where to export custom media - Added: New `importFrom` option to specify where to import custom media - Added: Support for PostCSS v7 - Added: Support for Node v6+ # 6.0.0 (May 12, 2017) - Added: compatibility with postcss v6.x # 5.0.1 (February 3, 2016) - Fixed: circular dependencies are properly detected ([#17](https://github.com/postcss/postcss-custom-media/pull/17)) # 5.0.0 (August 25, 2015) - Removed: compatibility with postcss v4.x - Added: compatibility with postcss v5.x # 4.1.0 (06 30, 2015) - Added: Allow custom media to reference each other ([#10](https://github.com/postcss/postcss-custom-media/pull/10)) # 4.0.0 (May 17, 2015) - Changed: warning messages are now sent via postcss messages api (^4.1.0) - Added: automatic custom media `--` prefixing ([#11](https://github.com/postcss/postcss-custom-media/issues/11)) - Added: `preserve` allows you to preserve custom media query defintions - Added: `appendExtensions` allows you (when `preserve` is truthy) to append your extensions as media queries # 3.0.0 (January 29, 2015) - Added: compatibility with postcss v4.x - Removed: compatibility with postcss v3.x # 2.0.0 [Yanked] _You never saw this version (this is a bad release that points to 1.0.0)._ # 1.3.0 (November 25, 2014) - Changed: better gnu message # 1.2.1 (October 9, 2014) - Fixed: npm description # 1.2.0 (October 1, 2014) - Added: support for multiples media in query list (ref [#rework-custom-media/5](https://github.com/reworkcss/rework-custom-media/pull/5)) # 1.1.0 (September 30, 2014) - Added: support for js-defined media queries (fix [#3](https://github.com/postcss/postcss-custom-media/issues/3)) # 1.0.1 (September 16, 2014) - Added: Allow whitespace around custom media name (fix [#2](https://github.com/postcss/postcss-custom-media/issues/2)) # 1.0.0 (August 12, 2014) ✨ First release based on [rework-custom-media](https://github.com/reworkcss/rework-custom-media) v0.1.1 ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to PostCSS Custom Media You want to help? You rock! Now, take a moment to be sure your contributions make sense to everyone else. ## Reporting Issues Found a problem? Want a new feature? - See if your issue or idea has [already been reported]. - Provide a [reduced test case] or a [live example]. Remember, a bug is a _demonstrable problem_ caused by _our_ code. ## Submitting Pull Requests Pull requests are the greatest contributions, so be sure they are focused in scope and avoid unrelated commits. 1. To begin; [fork this project], clone your fork, and add our upstream. ```bash # Clone your fork of the repo into the current directory git clone git@github.com:YOUR_USER/postcss-custom-media.git # Navigate to the newly cloned directory cd postcss-custom-media # Assign the original repo to a remote called "upstream" git remote add upstream git@github.com:postcss/postcss-custom-media.git # Install the tools necessary for testing npm install ``` 2. Create a branch for your feature or fix: ```bash # Move into a new branch for your feature git checkout -b feature/thing ``` ```bash # Move into a new branch for your fix git checkout -b fix/something ``` 3. If your code follows our practices, then push your feature branch: ```bash # Test current code npm test ``` ```bash # Push the branch for your new feature git push origin feature/thing ``` ```bash # Or, push the branch for your update git push origin update/something ``` That’s it! Now [open a pull request] with a clear title and description. [already been reported]: issues [fork this project]: fork [live example]: https://codepen.io/pen [open a pull request]: https://help.github.com/articles/using-pull-requests/ [reduced test case]: https://css-tricks.com/reduced-test-cases/ ================================================ FILE: INSTALL.md ================================================ # Installing PostCSS Custom Media [PostCSS Custom Media] runs in all Node environments, with special instructions for: | [Node](#node) | [PostCSS CLI](#postcss-cli) | [Webpack](#webpack) | [Create React App](#create-react-app) | [Gulp](#gulp) | [Grunt](#grunt) | | --- | --- | --- | --- | --- | --- | ## Node Add [PostCSS Custom Media] to your project: ```bash npm install postcss-custom-media --save-dev ``` Use [PostCSS Custom Media] to process your CSS: ```js const postcssCustomMedia = require('postcss-custom-media'); postcssCustomMedia.process(YOUR_CSS /*, processOptions, pluginOptions */); ``` Or use it as a [PostCSS] plugin: ```js const postcss = require('postcss'); const postcssCustomMedia = require('postcss-custom-media'); postcss([ postcssCustomMedia(/* pluginOptions */) ]).process(YOUR_CSS /*, processOptions */); ``` ## PostCSS CLI Add [PostCSS CLI] to your project: ```bash npm install postcss-cli --save-dev ``` Use [PostCSS Custom Media] in your `postcss.config.js` configuration file: ```js const postcssCustomMedia = require('postcss-custom-media'); module.exports = { plugins: [ postcssCustomMedia(/* pluginOptions */) ] } ``` ## Webpack Add [PostCSS Loader] to your project: ```bash npm install postcss-loader --save-dev ``` Use [PostCSS Custom Media] in your Webpack configuration: ```js const postcssCustomMedia = require('postcss-custom-media'); module.exports = { module: { rules: [ { test: /\.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { importLoaders: 1 } }, { loader: 'postcss-loader', options: { ident: 'postcss', plugins: () => [ postcssCustomMedia(/* pluginOptions */) ] } } ] } ] } } ``` ## Create React App Add [React App Rewired] and [React App Rewire PostCSS] to your project: ```bash npm install react-app-rewired react-app-rewire-postcss --save-dev ``` Use [React App Rewire PostCSS] and [PostCSS Custom Media] in your `config-overrides.js` file: ```js const reactAppRewirePostcss = require('react-app-rewire-postcss'); const postcssCustomMedia = require('postcss-custom-media'); module.exports = config => reactAppRewirePostcss(config, { plugins: () => [ postcssCustomMedia(/* pluginOptions */) ] }); ``` ## Gulp Add [Gulp PostCSS] to your project: ```bash npm install gulp-postcss --save-dev ``` Use [PostCSS Custom Media] in your Gulpfile: ```js const postcss = require('gulp-postcss'); const postcssCustomMedia = require('postcss-custom-media'); gulp.task('css', () => gulp.src('./src/*.css').pipe( postcss([ postcssCustomMedia(/* pluginOptions */) ]) ).pipe( gulp.dest('.') )); ``` ## Grunt Add [Grunt PostCSS] to your project: ```bash npm install grunt-postcss --save-dev ``` Use [PostCSS Custom Media] in your Gruntfile: ```js const postcssCustomMedia = require('postcss-custom-media'); grunt.loadNpmTasks('grunt-postcss'); grunt.initConfig({ postcss: { options: { use: [ postcssCustomMedia(/* pluginOptions */) ] }, dist: { src: '*.css' } } }); ``` [Gulp PostCSS]: https://github.com/postcss/gulp-postcss [Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss [PostCSS]: https://github.com/postcss/postcss [PostCSS CLI]: https://github.com/postcss/postcss-cli [PostCSS Loader]: https://github.com/postcss/postcss-loader [PostCSS Custom Media]: https://github.com/postcss/postcss-custom-media [React App Rewire PostCSS]: https://github.com/csstools/react-app-rewire-postcss [React App Rewired]: https://github.com/timarney/react-app-rewired ================================================ FILE: LICENSE.md ================================================ # The MIT License (MIT) Copyright © PostCSS Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
⚠️ PostCSS Custom Media was moved to @csstools/postcss-plugins. ⚠️
Read the announcement
# PostCSS Custom Media [PostCSS][postcss] [![NPM Version][npm-img]][npm-url] [![CSS Standard Status][css-img]][css-url] [![Build Status][cli-img]][cli-url] [![Support Chat][git-img]][git-url] [PostCSS Custom Media] lets you use Custom Media Queries in CSS, following the [CSS Media Queries] specification. ```pcss @custom-media --small-viewport (max-width: 30em); @media (--small-viewport) { /* styles for small viewport */ } /* becomes */ @media (max-width: 30em) { /* styles for small viewport */ } ``` ## Usage Add [PostCSS Custom Media] to your project: ```bash npm install postcss-custom-media --save-dev ``` Use [PostCSS Custom Media] to process your CSS: ```js const postcssCustomMedia = require('postcss-custom-media'); postcssCustomMedia.process(YOUR_CSS /*, processOptions, pluginOptions */); ``` Or use it as a [PostCSS] plugin: ```js const postcss = require('postcss'); const postcssCustomMedia = require('postcss-custom-media'); postcss([ postcssCustomMedia(/* pluginOptions */) ]).process(YOUR_CSS /*, processOptions */); ``` [PostCSS Custom Media] runs in all Node environments, with special instructions for: | [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 ### preserve The `preserve` option determines whether custom media and atrules using custom media should be preserved in their original form. ```pcss @custom-media --small-viewport (max-width: 30em); @media (--small-viewport) { /* styles for small viewport */ } /* becomes */ @custom-media --small-viewport (max-width: 30em); @media (max-width: 30em) { /* styles for small viewport */ } @media (--small-viewport) { /* styles for small viewport */ } ``` ### importFrom The `importFrom` option specifies sources where custom media can be imported from, which might be CSS, JS, and JSON files, functions, and directly passed objects. ```js postcssCustomMedia({ importFrom: 'path/to/file.css' // => @custom-selector --small-viewport (max-width: 30em); }); ``` ```pcss @media (max-width: 30em) { /* styles for small viewport */ } @media (--small-viewport) { /* styles for small viewport */ } ``` Multiple sources can be passed into this option, and they will be parsed in the order they are received. JavaScript files, JSON files, functions, and objects will need to namespace custom media using the `customMedia` or `custom-media` key. ```js postcssCustomMedia({ importFrom: [ 'path/to/file.css', 'and/then/this.js', 'and/then/that.json', { customMedia: { '--small-viewport': '(max-width: 30em)' } }, () => { const customMedia = { '--small-viewport': '(max-width: 30em)' }; return { customMedia }; } ] }); ``` ### exportTo The `exportTo` option specifies destinations where custom media can be exported to, which might be CSS, JS, and JSON files, functions, and directly passed objects. ```js postcssCustomMedia({ exportTo: 'path/to/file.css' // @custom-media --small-viewport (max-width: 30em); }); ``` Multiple destinations can be passed into this option, and they will be parsed in the order they are received. JavaScript files, JSON files, and objects will need to namespace custom media using the `customMedia` or `custom-media` key. ```js const cachedObject = { customMedia: {} }; postcssCustomMedia({ exportTo: [ 'path/to/file.css', // @custom-media --small-viewport (max-width: 30em); 'and/then/this.js', // module.exports = { customMedia: { '--small-viewport': '(max-width: 30em)' } } 'and/then/this.mjs', // export const customMedia = { '--small-viewport': '(max-width: 30em)' } } 'and/then/that.json', // { "custom-media": { "--small-viewport": "(max-width: 30em)" } } cachedObject, customMedia => { customMedia // { '--small-viewport': '(max-width: 30em)' } } ] }); ``` See example exports written to [CSS](test/export-media.css), [JS](test/export-media.js), [MJS](test/export-media.mjs), and [JSON](test/export-media.json). [cli-img]: https://img.shields.io/travis/postcss/postcss-custom-media/master.svg [cli-url]: https://travis-ci.org/postcss/postcss-custom-media [css-img]: https://cssdb.org/badge/custom-media-queries.svg [css-url]: https://cssdb.org/#custom-media-queries [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-custom-media.svg [npm-url]: https://www.npmjs.com/package/postcss-custom-media [CSS Media Queries]: https://drafts.csswg.org/mediaqueries-5/#custom-mq [PostCSS]: https://github.com/postcss/postcss [PostCSS Custom Media]: https://github.com/postcss/postcss-custom-media ================================================ FILE: index.js ================================================ import getCustomMediaFromRoot from './lib/custom-media-from-root'; import getCustomMediaFromImports from './lib/get-custom-media-from-imports'; import transformAtrules from './lib/transform-atrules'; import writeCustomMediaToExports from './lib/write-custom-media-to-exports'; const creator = opts => { // whether to preserve custom media and at-rules using them const preserve = 'preserve' in Object(opts) ? Boolean(opts.preserve) : false; // sources to import custom media from const importFrom = [].concat(Object(opts).importFrom || []); // destinations to export custom media to const exportTo = [].concat(Object(opts).exportTo || []); // promise any custom media are imported const customMediaImportsPromise = getCustomMediaFromImports(importFrom); return { postcssPlugin: 'postcss-custom-media', Once: async (root, helpers) => { // combine rules from root and from imports helpers.customMedia = Object.assign( await customMediaImportsPromise, getCustomMediaFromRoot(root, { preserve }) ); await writeCustomMediaToExports(helpers.customMedia, exportTo); }, AtRule: { media: (atrule, helpers) => { transformAtrules(atrule, {preserve}, helpers) } } } } creator.postcss = true export default creator ================================================ FILE: lib/custom-media-from-root.js ================================================ import mediaASTFromString from './media-ast-from-string'; // return custom selectors from the css root, conditionally removing them export default (root, opts) => { // initialize custom selectors const customMedias = {}; // for each custom selector atrule that is a child of the css root root.nodes.slice().forEach(node => { if (isCustomMedia(node)) { // extract the name and selectors from the params of the custom selector const [, name, selectors] = node.params.match(customMediaParamsRegExp); // write the parsed selectors to the custom selector customMedias[name] = mediaASTFromString(selectors); // conditionally remove the custom selector atrule if (!Object(opts).preserve) { node.remove(); } } }); return customMedias; }; // match the custom selector name const customMediaNameRegExp = /^custom-media$/i; // match the custom selector params const customMediaParamsRegExp = /^(--[A-z][\w-]*)\s+([\W\w]+)\s*$/; // whether the atrule is a custom selector const isCustomMedia = node => node.type === 'atrule' && customMediaNameRegExp.test(node.name) && customMediaParamsRegExp.test(node.params); ================================================ FILE: lib/get-custom-media-from-imports.js ================================================ import fs from 'fs'; import path from 'path'; import { parse } from 'postcss'; import getMediaAstFromMediaString from './media-ast-from-string'; import getCustomMedia from './custom-media-from-root'; /* Get Custom Media from CSS File /* ========================================================================== */ async function getCustomMediaFromCSSFile(from) { const css = await readFile(from); const root = parse(css, { from }); return getCustomMedia(root, { preserve: true }); } /* Get Custom Media from Object /* ========================================================================== */ function getCustomMediaFromObject(object) { const customMedia = Object.assign( {}, Object(object).customMedia, Object(object)['custom-media'] ); for (const key in customMedia) { customMedia[key] = getMediaAstFromMediaString(customMedia[key]); } return customMedia; } /* Get Custom Media from JSON file /* ========================================================================== */ async function getCustomMediaFromJSONFile(from) { const object = await readJSON(from); return getCustomMediaFromObject(object); } /* Get Custom Media from JS file /* ========================================================================== */ async function getCustomMediaFromJSFile(from) { const object = await import(from); return getCustomMediaFromObject(object); } /* Get Custom Media from Sources /* ========================================================================== */ export default function getCustomMediaFromSources(sources) { return sources.map(source => { if (source instanceof Promise) { return source; } else if (source instanceof Function) { return source(); } // read the source as an object const opts = source === Object(source) ? source : { from: String(source) }; // skip objects with custom media if (Object(opts).customMedia || Object(opts)['custom-media']) { return opts } // source pathname const from = path.resolve(String(opts.from || '')); // type of file being read from const type = (opts.type || path.extname(from).slice(1)).toLowerCase(); return { type, from }; }).reduce(async (customMedia, source) => { const { type, from } = await source; if (type === 'css' || type === 'pcss') { return Object.assign(await customMedia, await getCustomMediaFromCSSFile(from)); } if (type === 'js') { return Object.assign(await customMedia, await getCustomMediaFromJSFile(from)); } if (type === 'json') { return Object.assign(await customMedia, await getCustomMediaFromJSONFile(from)); } return Object.assign(await customMedia, getCustomMediaFromObject(await source)); }, {}); } /* Helper utilities /* ========================================================================== */ const readFile = from => new Promise((resolve, reject) => { fs.readFile(from, 'utf8', (error, result) => { if (error) { reject(error); } else { resolve(result); } }); }); const readJSON = async from => JSON.parse(await readFile(from)); ================================================ FILE: lib/media-ast-from-string.js ================================================ function parse(string, splitByAnd) { const array = []; let buffer = ''; let split = false; let func = 0; let i = -1; while (++i < string.length) { const char = string[i]; if (char === '(') { func += 1; } else if (char === ')') { if (func > 0) { func -= 1; } } else if (func === 0) { if (splitByAnd && andRegExp.test(buffer + char)) { split = true; } else if (!splitByAnd && char === ',') { split = true; } } if (split) { array.push(splitByAnd ? new MediaExpression(buffer + char) : new MediaQuery(buffer)); buffer = ''; split = false; } else { buffer += char } } if (buffer !== '') { array.push(splitByAnd ? new MediaExpression(buffer) : new MediaQuery(buffer)); } return array; } class MediaQueryList { constructor(string) { this.nodes = parse(string); } invert() { this.nodes.forEach(node => { node.invert(); }) return this; } clone() { return new MediaQueryList(String(this)); } toString() { return this.nodes.join(','); } } class MediaQuery { constructor(string) { const [, before, media, after ] = string.match(spaceWrapRegExp); const [, modifier = '', afterModifier = ' ', type = '', beforeAnd = '', and = '', beforeExpression = '', expression1 = '', expression2 = ''] = media.match(mediaRegExp) || []; const raws = { before, after, afterModifier, originalModifier: modifier || '', beforeAnd, and, beforeExpression }; const nodes = parse(expression1 || expression2, true); Object.assign(this, { modifier, type, raws, nodes }); } clone(overrides) { const instance = new MediaQuery(String(this)); Object.assign(instance, overrides); return instance; } invert() { this.modifier = this.modifier ? '' : this.raws.originalModifier; return this; } toString() { const { raws } = this; return `${raws.before}${this.modifier}${this.modifier ? `${raws.afterModifier}` : ''}${this.type}${raws.beforeAnd}${raws.and}${raws.beforeExpression}${this.nodes.join('')}${this.raws.after}`; } } class MediaExpression { constructor(string) { const [, value, after = '', and = '', afterAnd = '' ] = string.match(andRegExp) || [null, string]; const raws = { after, and, afterAnd }; Object.assign(this, { value, raws }); } clone(overrides) { const instance = new MediaExpression(String(this)); Object.assign(instance, overrides); return instance; } toString() { const { raws } = this; return `${this.value}${raws.after}${raws.and}${raws.afterAnd}`; } } const modifierRE = '(not|only)'; const typeRE = '(all|print|screen|speech)'; const noExpressionRE = '([\\W\\w]*)'; const expressionRE = '([\\W\\w]+)'; const noSpaceRE = '(\\s*)'; const spaceRE = '(\\s+)'; const andRE = '(?:(\\s+)(and))'; const andRegExp = new RegExp(`^${expressionRE}(?:${andRE}${spaceRE})$`, 'i'); const spaceWrapRegExp = new RegExp(`^${noSpaceRE}${noExpressionRE}${noSpaceRE}$`); const mediaRegExp = new RegExp(`^(?:${modifierRE}${spaceRE})?(?:${typeRE}(?:${andRE}${spaceRE}${expressionRE})?|${expressionRE})$`, 'i'); export default string => new MediaQueryList(string); ================================================ FILE: lib/transform-atrules.js ================================================ import transformMediaList from "./transform-media-list"; import mediaASTFromString from "./media-ast-from-string"; // transform custom pseudo selectors with custom selectors export default (atrule, { preserve }, { customMedia }) => { if (customPseudoRegExp.test(atrule.params)) { // prevent infinite loops when using 'preserve' option if (!atrule[visitedFlag]) { atrule[visitedFlag] = true; const mediaAST = mediaASTFromString(atrule.params); const params = String(transformMediaList(mediaAST, customMedia)); if (preserve) { // keep an original copy const node = atrule.cloneAfter(); node[visitedFlag] = true; } // replace the variable with the params from @custom-media rule // skip if the variable is undefined if (params != null) { atrule.params = params; } } } }; const visitedFlag = Symbol("customMediaVisited"); const customPseudoRegExp = /\(--[A-z][\w-]*\)/; ================================================ FILE: lib/transform-media-list.js ================================================ // return transformed medias, replacing custom pseudo medias with custom medias export default function transformMediaList(mediaList, customMedias) { let index = mediaList.nodes.length - 1; while (index >= 0) { const transformedMedias = transformMedia(mediaList.nodes[index], customMedias); if (transformedMedias.length) { mediaList.nodes.splice(index, 1, ...transformedMedias); } --index; } return mediaList; } // return custom pseudo medias replaced with custom medias function transformMedia(media, customMedias) { const transpiledMedias = []; for (const index in media.nodes) { const { value, nodes } = media.nodes[index]; const key = value.replace(customPseudoRegExp, '$1'); if (key in customMedias) { for (const replacementMedia of customMedias[key].nodes) { // use the first available modifier unless they cancel each other out const modifier = media.modifier !== replacementMedia.modifier ? media.modifier || replacementMedia.modifier : ''; const mediaClone = media.clone({ modifier, // conditionally use the raws from the first available modifier raws: !modifier || media.modifier ? { ...media.raws } : { ...replacementMedia.raws }, type: media.type || replacementMedia.type, }); // conditionally include more replacement raws when the type is present if (mediaClone.type === replacementMedia.type) { Object.assign(mediaClone.raws, { and: replacementMedia.raws.and, beforeAnd: replacementMedia.raws.beforeAnd, beforeExpression: replacementMedia.raws.beforeExpression }); } mediaClone.nodes.splice(index, 1, ...replacementMedia.clone().nodes.map(node => { // use raws and spacing from the current usage if (media.nodes[index].raws.and) { node.raws = { ...media.nodes[index].raws }; } node.spaces = { ...media.nodes[index].spaces }; return node; })); // remove the currently transformed key to prevent recursion const nextCustomMedia = getCustomMediasWithoutKey(customMedias, key); const retranspiledMedias = transformMedia(mediaClone, nextCustomMedia); if (retranspiledMedias.length) { transpiledMedias.push(...retranspiledMedias); } else { transpiledMedias.push(mediaClone); } } return transpiledMedias; } else if (nodes && nodes.length) { transformMediaList(media.nodes[index], customMedias); } } return transpiledMedias; } const customPseudoRegExp = /\((--[A-z][\w-]*)\)/; const getCustomMediasWithoutKey = (customMedias, key) => { const nextCustomMedias = Object.assign({}, customMedias); delete nextCustomMedias[key]; return nextCustomMedias; }; ================================================ FILE: lib/write-custom-media-to-exports.js ================================================ import fs from 'fs'; import path from 'path'; /* Write Custom Media from CSS File /* ========================================================================== */ async function writeCustomMediaToCssFile(to, customMedia) { const cssContent = Object.keys(customMedia).reduce((cssLines, name) => { cssLines.push(`@custom-media ${name} ${customMedia[name]};`); return cssLines; }, []).join('\n'); const css = `${cssContent}\n`; await writeFile(to, css); } /* Write Custom Media from JSON file /* ========================================================================== */ async function writeCustomMediaToJsonFile(to, customMedia) { const jsonContent = JSON.stringify({ 'custom-media': customMedia }, null, ' '); const json = `${jsonContent}\n`; await writeFile(to, json); } /* Write Custom Media from Common JS file /* ========================================================================== */ async function writeCustomMediaToCjsFile(to, customMedia) { const jsContents = Object.keys(customMedia).reduce((jsLines, name) => { jsLines.push(`\t\t'${escapeForJS(name)}': '${escapeForJS(customMedia[name])}'`); return jsLines; }, []).join(',\n'); const js = `module.exports = {\n\tcustomMedia: {\n${jsContents}\n\t}\n};\n`; await writeFile(to, js); } /* Write Custom Media from Module JS file /* ========================================================================== */ async function writeCustomMediaToMjsFile(to, customMedia) { const mjsContents = Object.keys(customMedia).reduce((mjsLines, name) => { mjsLines.push(`\t'${escapeForJS(name)}': '${escapeForJS(customMedia[name])}'`); return mjsLines; }, []).join(',\n'); const mjs = `export const customMedia = {\n${mjsContents}\n};\n`; await writeFile(to, mjs); } /* Write Custom Media to Exports /* ========================================================================== */ export default function writeCustomMediaToExports(customMedia, destinations) { return Promise.all(destinations.map(async destination => { if (destination instanceof Function) { await destination(defaultCustomMediaToJSON(customMedia)); } else { // read the destination as an object const opts = destination === Object(destination) ? destination : { to: String(destination) }; // transformer for custom media into a JSON-compatible object const toJSON = opts.toJSON || defaultCustomMediaToJSON; if ('customMedia' in opts) { // write directly to an object as customMedia opts.customMedia = toJSON(customMedia); } else if ('custom-media' in opts) { // write directly to an object as custom-media opts['custom-media'] = toJSON(customMedia); } else { // destination pathname const to = String(opts.to || ''); // type of file being written to const type = (opts.type || path.extname(to).slice(1)).toLowerCase(); // transformed custom media const customMediaJSON = toJSON(customMedia); if (type === 'css') { await writeCustomMediaToCssFile(to, customMediaJSON); } if (type === 'js') { await writeCustomMediaToCjsFile(to, customMediaJSON); } if (type === 'json') { await writeCustomMediaToJsonFile(to, customMediaJSON); } if (type === 'mjs') { await writeCustomMediaToMjsFile(to, customMediaJSON); } } } })); } /* Helper utilities /* ========================================================================== */ const defaultCustomMediaToJSON = customMedia => { return Object.keys(customMedia).reduce((customMediaJSON, key) => { customMediaJSON[key] = String(customMedia[key]); return customMediaJSON; }, {}); }; const writeFile = (to, text) => new Promise((resolve, reject) => { fs.writeFile(to, text, error => { if (error) { reject(error); } else { resolve(); } }); }); const escapeForJS = string => string.replace(/\\([\s\S])|(')/g, '\\$1$2').replace(/\n/g, '\\n').replace(/\r/g, '\\r'); ================================================ FILE: package.json ================================================ { "name": "postcss-custom-media", "version": "8.0.0", "description": "Use Custom Media Queries in CSS", "author": "Jonathan Neal ", "contributors": [ "Maxime Thirouin" ], "license": "MIT", "repository": "postcss/postcss-custom-media", "homepage": "https://github.com/postcss/postcss-custom-media#readme", "bugs": "https://github.com/postcss/postcss-custom-media/issues", "main": "index.cjs.js", "module": "index.es.mjs", "files": [ "index.cjs.js", "index.es.mjs" ], "scripts": { "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 lib/*.js --cache --ignore-path .gitignore --quiet", "test:tape": "postcss-tape" }, "engines": { "node": ">=10.0.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "devDependencies": { "@babel/core": "^7.11.6", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/preset-env": "^7.11.5", "@rollup/plugin-babel": "^5.2.1", "babel-eslint": "^10.1.0", "eslint": "^7.10.0", "postcss": "^8.1.0", "postcss-tape": "^6.0.0", "pre-commit": "^1.2.2", "rollup": "^2.28.2" }, "eslintConfig": { "env": { "browser": true, "es6": true, "node": true }, "extends": "eslint:recommended", "parser": "babel-eslint", "parserOptions": { "ecmaVersion": 2018, "impliedStrict": true, "sourceType": "module" } }, "keywords": [ "postcss", "css", "postcss-plugin", "custom", "media", "query", "queries", "w3c", "csswg", "atrule", "at-rule", "specification" ] } ================================================ FILE: test/basic.css ================================================ @custom-media --mq-a (max-width: 30em), (max-height: 30em); @custom-media --mq-b screen and (max-width: 30em); @custom-media --not-mq-a not all and (--mq-a); @media (--mq-a) { body { order: 1; } } @media (--mq-b) { body { order: 1; } } @media (--mq-a), (--mq-a) { body { order: 1; } } @media not all and (--mq-a) { body { order: 2; } } @media (--not-mq-a) { body { order: 1; } } @media not all and (--not-mq-a) { body { order: 2; } } @custom-media --circular-mq-a (--circular-mq-b); @custom-media --circular-mq-b (--circular-mq-a); @media (--circular-mq-a) { body { order: 3; } } @media (--circular-mq-b) { body { order: 4; } } @media (--unresolved-mq) { body { order: 5; } } @custom-media --min (min-width: 320px); @custom-media --max (max-width: 640px); @media (--min) and (--max) { body { order: 6; } } @custom-media --concat (min-width: 320px) and (max-width: 640px); @media (--concat) { body { order: 7; } } @media (--concat) and (min-aspect-ratio: 16/9) { body { order: 8; } } ================================================ FILE: test/basic.expect.css ================================================ @media (max-width: 30em),(max-height: 30em) { body { order: 1; } } @media screen and (max-width: 30em) { body { order: 1; } } @media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { body { order: 1; } } @media not all and (max-width: 30em),not all and (max-height: 30em) { body { order: 2; } } @media not all and (max-width: 30em),not all and (max-height: 30em) { body { order: 1; } } @media all and (max-width: 30em),all and (max-height: 30em) { body { order: 2; } } @media (--circular-mq-a) { body { order: 3; } } @media (--circular-mq-b) { body { order: 4; } } @media (--unresolved-mq) { body { order: 5; } } @media (min-width: 320px) and (max-width: 640px) { body { order: 6; } } @media (min-width: 320px) and (max-width: 640px) { body { order: 7; } } @media (min-width: 320px) and (max-width: 640px) and (min-aspect-ratio: 16/9) { body { order: 8; } } ================================================ FILE: test/basic.import.expect.css ================================================ ================================================ FILE: test/basic.preserve.expect.css ================================================ @custom-media --mq-a (max-width: 30em), (max-height: 30em); @custom-media --mq-b screen and (max-width: 30em); @custom-media --not-mq-a not all and (--mq-a); @media (max-width: 30em),(max-height: 30em) { body { order: 1; } } @media (--mq-a) { body { order: 1; } } @media screen and (max-width: 30em) { body { order: 1; } } @media (--mq-b) { body { order: 1; } } @media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { body { order: 1; } } @media (--mq-a), (--mq-a) { body { order: 1; } } @media not all and (max-width: 30em),not all and (max-height: 30em) { body { order: 2; } } @media not all and (--mq-a) { body { order: 2; } } @media not all and (max-width: 30em),not all and (max-height: 30em) { body { order: 1; } } @media (--not-mq-a) { body { order: 1; } } @media all and (max-width: 30em),all and (max-height: 30em) { body { order: 2; } } @media not all and (--not-mq-a) { body { order: 2; } } @custom-media --circular-mq-a (--circular-mq-b); @custom-media --circular-mq-b (--circular-mq-a); @media (--circular-mq-a) { body { order: 3; } } @media (--circular-mq-a) { body { order: 3; } } @media (--circular-mq-b) { body { order: 4; } } @media (--circular-mq-b) { body { order: 4; } } @media (--unresolved-mq) { body { order: 5; } } @media (--unresolved-mq) { body { order: 5; } } @custom-media --min (min-width: 320px); @custom-media --max (max-width: 640px); @media (min-width: 320px) and (max-width: 640px) { body { order: 6; } } @media (--min) and (--max) { body { order: 6; } } @custom-media --concat (min-width: 320px) and (max-width: 640px); @media (min-width: 320px) and (max-width: 640px) { body { order: 7; } } @media (--concat) { body { order: 7; } } @media (min-width: 320px) and (max-width: 640px) and (min-aspect-ratio: 16/9) { body { order: 8; } } @media (--concat) and (min-aspect-ratio: 16/9) { body { order: 8; } } ================================================ FILE: test/export-media.css ================================================ @custom-media --mq-a (max-width: 30em), (max-height: 30em); @custom-media --mq-b screen and (max-width: 30em); @custom-media --not-mq-a not all and (--mq-a); @custom-media --circular-mq-a (--circular-mq-b); @custom-media --circular-mq-b (--circular-mq-a); @custom-media --min (min-width: 320px); @custom-media --max (max-width: 640px); @custom-media --concat (min-width: 320px) and (max-width: 640px); ================================================ FILE: test/export-media.js ================================================ module.exports = { customMedia: { '--mq-a': '(max-width: 30em), (max-height: 30em)', '--mq-b': 'screen and (max-width: 30em)', '--not-mq-a': 'not all and (--mq-a)', '--circular-mq-a': '(--circular-mq-b)', '--circular-mq-b': '(--circular-mq-a)', '--min': '(min-width: 320px)', '--max': '(max-width: 640px)', '--concat': '(min-width: 320px) and (max-width: 640px)' } }; ================================================ FILE: test/export-media.json ================================================ { "custom-media": { "--mq-a": "(max-width: 30em), (max-height: 30em)", "--mq-b": "screen and (max-width: 30em)", "--not-mq-a": "not all and (--mq-a)", "--circular-mq-a": "(--circular-mq-b)", "--circular-mq-b": "(--circular-mq-a)", "--min": "(min-width: 320px)", "--max": "(max-width: 640px)", "--concat": "(min-width: 320px) and (max-width: 640px)" } } ================================================ FILE: test/export-media.mjs ================================================ export const customMedia = { '--mq-a': '(max-width: 30em), (max-height: 30em)', '--mq-b': 'screen and (max-width: 30em)', '--not-mq-a': 'not all and (--mq-a)', '--circular-mq-a': '(--circular-mq-b)', '--circular-mq-b': '(--circular-mq-a)', '--min': '(min-width: 320px)', '--max': '(max-width: 640px)', '--concat': '(min-width: 320px) and (max-width: 640px)' }; ================================================ FILE: test/import-css.css ================================================ ================================================ FILE: test/import-media.css ================================================ @custom-media --mq-a (max-width: 30em), (max-height: 30em); @custom-media --not-mq-a not all and (--mq-a); ================================================ FILE: test/import-media.js ================================================ module.exports = { customMedia: { '--mq-a': '(max-width: 30em), (max-height: 30em)', '--not-mq-a': 'not all and (--mq-a)' } } ================================================ FILE: test/import-media.json ================================================ { "customMedia": { "--mq-a": "(max-width: 30em), (max-height: 30em)", "--not-mq-a": "not all and (--mq-a)" } } ================================================ FILE: test/import.css ================================================ @media (--mq-a) { body { order: 1; } } @media (--mq-a), (--mq-a) { body { order: 1; } } @media not all and (--mq-a) { body { order: 2; } } @media (--not-mq-a) { body { order: 1; } } @media not all and (--not-mq-a) { body { order: 2; } } ================================================ FILE: test/import.empty.expect.css ================================================ @media (--mq-a) { body { order: 1; } } @media (--mq-a), (--mq-a) { body { order: 1; } } @media not all and (--mq-a) { body { order: 2; } } @media (--not-mq-a) { body { order: 1; } } @media not all and (--not-mq-a) { body { order: 2; } } ================================================ FILE: test/import.expect.css ================================================ @media (max-width: 30em),(max-height: 30em) { body { order: 1; } } @media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { body { order: 1; } } @media not all and (max-width: 30em),not all and (max-height: 30em) { body { order: 2; } } @media not all and (max-width: 30em),not all and (max-height: 30em) { body { order: 1; } } @media all and (max-width: 30em),all and (max-height: 30em) { body { order: 2; } } ================================================ FILE: test/import.plugin.expect.css ================================================ @media (max-width: 30em),(max-height: 30em) { body { order: 1; } } @media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { body { order: 1; } } @media not all and (max-width: 30em),not all and (max-height: 30em) { body { order: 2; } } @media not all and (max-width: 30em),not all and (max-height: 30em) { body { order: 1; } } @media all and (max-width: 30em),all and (max-height: 30em) { body { order: 2; } }