diff --git a/.github/ISSUE_TEMPLATE/css-issue.yml b/.github/ISSUE_TEMPLATE/css-issue.yml
index 135995ae7..8107c8b73 100644
--- a/.github/ISSUE_TEMPLATE/css-issue.yml
+++ b/.github/ISSUE_TEMPLATE/css-issue.yml
@@ -70,8 +70,11 @@ body:
- PostCSS Color Hex Alpha
- PostCSS Color Mod Function
- PostCSS Custom Media Queries
+ - PostCSS Custom Media Queries Import/Export
- PostCSS Custom Properties
+ - PostCSS Custom Properties Import/Export
- PostCSS Custom Selectors
+ - PostCSS Custom Selectors Import/Export
- PostCSS Design Tokens
- PostCSS Dir Pseudo Class
- PostCSS Double Position Gradients
diff --git a/.github/ISSUE_TEMPLATE/plugin-issue.yml b/.github/ISSUE_TEMPLATE/plugin-issue.yml
index 2fa9f2915..fa97f0a38 100644
--- a/.github/ISSUE_TEMPLATE/plugin-issue.yml
+++ b/.github/ISSUE_TEMPLATE/plugin-issue.yml
@@ -72,8 +72,11 @@ body:
- PostCSS Color Hex Alpha
- PostCSS Color Mod Function
- PostCSS Custom Media Queries
+ - PostCSS Custom Media Queries Import/Export
- PostCSS Custom Properties
+ - PostCSS Custom Properties Import/Export
- PostCSS Custom Selectors
+ - PostCSS Custom Selectors Import/Export
- PostCSS Design Tokens
- PostCSS Dir Pseudo Class
- PostCSS Double Position Gradients
diff --git a/.github/labeler.yml b/.github/labeler.yml
index c874c1372..90ae46a52 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -216,5 +216,13 @@
- plugins/postcss-trigonometric-functions/**
- experimental/postcss-trigonometric-functions/**
+"plugins/import-export":
+ - plugins/postcss-custom-media-queries-import-export/**
+ - experimental/postcss-custom-media-queries-import-export/**
+ - plugins/postcss-custom-properties-import-export/**
+ - experimental/postcss-custom-properties-import-export/**
+ - plugins/postcss-custom-selectors-import-export/**
+ - experimental/postcss-custom-selectors-import-export/**
+
"sites/postcss-preset-env":
- sites/postcss-preset-env/**
diff --git a/package-lock.json b/package-lock.json
index c5371965e..47a4b45d6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1923,6 +1923,18 @@
"resolved": "plugins/postcss-conditional-values",
"link": true
},
+ "node_modules/@csstools/postcss-custom-media-import-export": {
+ "resolved": "plugins/postcss-custom-media-import-export",
+ "link": true
+ },
+ "node_modules/@csstools/postcss-custom-properties-import-export": {
+ "resolved": "plugins/postcss-custom-properties-import-export",
+ "link": true
+ },
+ "node_modules/@csstools/postcss-custom-selectors-import-export": {
+ "resolved": "plugins/postcss-custom-selectors-import-export",
+ "link": true
+ },
"node_modules/@csstools/postcss-design-tokens": {
"resolved": "plugins/postcss-design-tokens",
"link": true
@@ -7392,6 +7404,27 @@
"postcss": "^8.4"
}
},
+ "plugins/postcss-custom-media-import-export": {
+ "name": "@csstools/postcss-custom-media-import-export",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "devDependencies": {
+ "postcss-custom-media": "^9.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
"plugins/postcss-custom-properties": {
"version": "13.0.0",
"license": "MIT",
@@ -7412,6 +7445,25 @@
"postcss": "^8.4"
}
},
+ "plugins/postcss-custom-properties-import-export": {
+ "name": "@csstools/postcss-custom-properties-import-export",
+ "version": "1.0.0",
+ "license": "MIT",
+ "devDependencies": {
+ "postcss-custom-properties": "^13.0.0",
+ "postcss-import": "^15.0.0"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
"plugins/postcss-custom-selectors": {
"version": "7.0.0",
"license": "MIT",
@@ -7429,6 +7481,27 @@
"postcss": "^8.4"
}
},
+ "plugins/postcss-custom-selectors-import-export": {
+ "name": "@csstools/postcss-custom-selectors-import-export",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.4"
+ },
+ "devDependencies": {
+ "postcss-custom-selectors": "^7.0.0"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
"plugins/postcss-design-tokens": {
"name": "@csstools/postcss-design-tokens",
"version": "1.2.0",
@@ -9282,6 +9355,27 @@
"postcss-value-parser": "^4.2.0"
}
},
+ "@csstools/postcss-custom-media-import-export": {
+ "version": "file:plugins/postcss-custom-media-import-export",
+ "requires": {
+ "postcss-custom-media": "^9.0.1",
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "@csstools/postcss-custom-properties-import-export": {
+ "version": "file:plugins/postcss-custom-properties-import-export",
+ "requires": {
+ "postcss-custom-properties": "^13.0.0",
+ "postcss-import": "^15.0.0"
+ }
+ },
+ "@csstools/postcss-custom-selectors-import-export": {
+ "version": "file:plugins/postcss-custom-selectors-import-export",
+ "requires": {
+ "postcss-custom-selectors": "^7.0.0",
+ "postcss-selector-parser": "^6.0.4"
+ }
+ },
"@csstools/postcss-design-tokens": {
"version": "file:plugins/postcss-design-tokens",
"requires": {
diff --git a/plugins/postcss-custom-media-import-export/.gitignore b/plugins/postcss-custom-media-import-export/.gitignore
new file mode 100644
index 000000000..7172b04f1
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+package-lock.json
+yarn.lock
+*.result.css
+*.result.css.map
+dist/*
diff --git a/plugins/postcss-custom-media-import-export/.nvmrc b/plugins/postcss-custom-media-import-export/.nvmrc
new file mode 100644
index 000000000..f0b10f153
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/.nvmrc
@@ -0,0 +1 @@
+v16.13.1
diff --git a/plugins/postcss-custom-media-import-export/.tape.cjs b/plugins/postcss-custom-media-import-export/.tape.cjs
new file mode 100644
index 000000000..3312d9165
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/.tape.cjs
@@ -0,0 +1,250 @@
+const postcssTape = require('../../packages/postcss-tape/dist/index.cjs');
+const plugin = require('@csstools/postcss-custom-media-import-export');
+const polyfillPlugin = require('postcss-custom-media');
+const fs = require('fs');
+
+postcssTape(plugin)({
+ '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:with-polyfill-plugin': {
+ message: 'works correctly together with the polyfill',
+ plugins: [
+ plugin({
+ importFrom: {
+ customMedia: {
+ '--mq-a': '(max-width: 30em), (max-height: 30em)',
+ '--not-mq-a': 'not all and (--mq-a)'
+ }
+ }
+ }),
+ polyfillPlugin(),
+ ]
+ },
+ 'import:imported-styles-override-document-styles:true': {
+ message: 'supports { importedStylesOverrideDocumentStyles: true } usage',
+ options: {
+ importedStylesOverrideDocumentStyles: true,
+ importFrom: {
+ customMedia: {
+ '--mq-a': '(max-width: 30em), (max-height: 30em)',
+ '--not-mq-a': 'not all and (--mq-a)'
+ }
+ }
+ }
+ },
+ 'import:imported-styles-override-document-styles:false': {
+ message: 'supports { importedStylesOverrideDocumentStyles: false } usage',
+ options: {
+ importedStylesOverrideDocumentStyles: false,
+ 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)'
+ }
+ };
+ }
+ },
+ 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)'
+ }
+ })
+ });
+ }
+ },
+ result: 'import.result.css'
+ },
+ 'import:json': {
+ message: 'supports { importFrom: "test/import-media.json" } usage',
+ options: {
+ importFrom: 'test/import-media.json'
+ },
+ result: 'import.result.css'
+ },
+ 'import:js': {
+ message: 'supports { importFrom: "test/import-media.js" } usage',
+ options: {
+ importFrom: 'test/import-media.js'
+ },
+ result: 'import.result.css'
+ },
+ 'import:css': {
+ message: 'supports { importFrom: "test/import-media.css" } usage',
+ options: {
+ importFrom: 'test/import-media.css'
+ },
+ result: 'import.result.css'
+ },
+ 'import:css-from': {
+ message: 'supports { importFrom: { from: "test/import-media.css" } } usage',
+ options: {
+ importFrom: { from: 'test/import-media.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' }]
+ },
+ 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
+ })
+ },
+ 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');
+ }
+ }
+ },
+ 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();
+ }
+ });
+ }
+ },
+ result: 'basic.result.css'
+ },
+ 'basic:export-json': {
+ message: 'supports { exportTo: "test/export-media.json" } usage',
+ options: {
+ exportTo: 'test/export-media.json'
+ },
+ before() {
+ global.__exportMediaString = fs.readFileSync('test/export-media.json', 'utf8');
+ },
+ after() {
+ if (global.__exportMediaString !== 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'
+ },
+ before() {
+ global.__exportMediaString = fs.readFileSync('test/export-media.js', 'utf8');
+ },
+ after() {
+ if (global.__exportMediaString !== 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'
+ },
+ before() {
+ global.__exportMediaString = fs.readFileSync('test/export-media.mjs', 'utf8');
+ },
+ after() {
+ if (global.__exportMediaString !== 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'
+ },
+ before() {
+ global.__exportMediaString = fs.readFileSync('test/export-media.css', 'utf8');
+ },
+ after() {
+ if (global.__exportMediaString !== 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' }
+ },
+ before() {
+ global.__exportMediaString = fs.readFileSync('test/export-media.css', 'utf8');
+ },
+ after() {
+ if (global.__exportMediaString !== 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' }
+ },
+ before() {
+ global.__exportMediaString = fs.readFileSync('test/export-media.css', 'utf8');
+ },
+ after() {
+ if (global.__exportMediaString !== fs.readFileSync('test/export-media.css', 'utf8')) {
+ throw new Error('The original file did not match the freshly exported copy');
+ }
+ }
+ }
+});
diff --git a/plugins/postcss-custom-media-import-export/CHANGELOG.md b/plugins/postcss-custom-media-import-export/CHANGELOG.md
new file mode 100644
index 000000000..bc8b2132c
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changes to PostCSS Custom Media Import/Export
+
+### 1.0.0 (Unreleased)
+
+- Initial version
diff --git a/plugins/postcss-custom-media-import-export/INSTALL.md b/plugins/postcss-custom-media-import-export/INSTALL.md
new file mode 100644
index 000000000..c6dc188a9
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/INSTALL.md
@@ -0,0 +1,257 @@
+# Installing PostCSS Custom Media Import/Export
+
+[PostCSS Custom Media Import/Export] runs in all Node environments, with special instructions for:
+
+- [Node](#node)
+- [PostCSS CLI](#postcss-cli)
+- [PostCSS Load Config](#postcss-load-config)
+- [Webpack](#webpack)
+- [Create React App](#create-react-app)
+- [Next.js](#nextjs)
+- [Gulp](#gulp)
+- [Grunt](#grunt)
+
+## Node
+
+Add [PostCSS Custom Media Import/Export] to your project:
+
+```bash
+npm install postcss @csstools/postcss-custom-media-import-export --save-dev
+```
+
+Use it as a [PostCSS] plugin:
+
+```js
+// commonjs
+const postcss = require('postcss');
+const postcssCustomMediaImportExport = require('@csstools/postcss-custom-media-import-export');
+
+postcss([
+ postcssCustomMediaImportExport(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+```js
+// esm
+import postcss from 'postcss';
+import postcssCustomMediaImportExport from '@csstools/postcss-custom-media-import-export';
+
+postcss([
+ postcssCustomMediaImportExport(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+## PostCSS CLI
+
+Add [PostCSS CLI] to your project:
+
+```bash
+npm install postcss-cli @csstools/postcss-custom-media-import-export --save-dev
+```
+
+Use [PostCSS Custom Media Import/Export] in your `postcss.config.js` configuration file:
+
+```js
+const postcssCustomMediaImportExport = require('@csstools/postcss-custom-media-import-export');
+
+module.exports = {
+ plugins: [
+ postcssCustomMediaImportExport(/* pluginOptions */)
+ ]
+}
+```
+
+## PostCSS Load Config
+
+If your framework/CLI supports [`postcss-load-config`](https://github.com/postcss/postcss-load-config).
+
+```bash
+npm install @csstools/postcss-custom-media-import-export --save-dev
+```
+
+`package.json`:
+
+```json
+{
+ "postcss": {
+ "plugins": {
+ "@csstools/postcss-custom-media-import-export": {}
+ }
+ }
+}
+```
+
+`.postcssrc.json`:
+
+```json
+{
+ "plugins": {
+ "@csstools/postcss-custom-media-import-export": {}
+ }
+}
+```
+
+_See the [README of `postcss-load-config`](https://github.com/postcss/postcss-load-config#usage) for more usage options._
+
+## Webpack
+
+_Webpack version 5_
+
+Add [PostCSS Loader] to your project:
+
+```bash
+npm install postcss-loader @csstools/postcss-custom-media-import-export --save-dev
+```
+
+Use [PostCSS Custom Media Import/Export] in your Webpack configuration:
+
+```js
+module.exports = {
+ module: {
+ rules: [
+ {
+ test: /\.css$/i,
+ use: [
+ "style-loader",
+ {
+ loader: "css-loader",
+ options: { importLoaders: 1 },
+ },
+ {
+ loader: "postcss-loader",
+ options: {
+ postcssOptions: {
+ plugins: [
+ [
+ "@csstools/postcss-custom-media-import-export",
+ {
+ // Options
+ },
+ ],
+ ],
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+};
+```
+
+## 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 @csstools/postcss-custom-media-import-export --save-dev
+```
+
+Use [React App Rewire PostCSS] and [PostCSS Custom Media Import/Export] in your
+`config-overrides.js` file:
+
+```js
+const reactAppRewirePostcss = require('react-app-rewire-postcss');
+const postcssCustomMediaImportExport = require('@csstools/postcss-custom-media-import-export');
+
+module.exports = config => reactAppRewirePostcss(config, {
+ plugins: () => [
+ postcssCustomMediaImportExport(/* pluginOptions */)
+ ]
+});
+```
+
+## Next.js
+
+Read the instructions on how to [customize the PostCSS configuration in Next.js](https://nextjs.org/docs/advanced-features/customizing-postcss-config)
+
+```bash
+npm install @csstools/postcss-custom-media-import-export --save-dev
+```
+
+Use [PostCSS Custom Media Import/Export] in your `postcss.config.json` file:
+
+```json
+{
+ "plugins": [
+ "@csstools/postcss-custom-media-import-export"
+ ]
+}
+```
+
+```json5
+{
+ "plugins": [
+ [
+ "@csstools/postcss-custom-media-import-export",
+ {
+ // Optionally add plugin options
+ }
+ ]
+ ]
+}
+```
+
+## Gulp
+
+Add [Gulp PostCSS] to your project:
+
+```bash
+npm install gulp-postcss @csstools/postcss-custom-media-import-export --save-dev
+```
+
+Use [PostCSS Custom Media Import/Export] in your Gulpfile:
+
+```js
+const postcss = require('gulp-postcss');
+const postcssCustomMediaImportExport = require('@csstools/postcss-custom-media-import-export');
+
+gulp.task('css', function () {
+ var plugins = [
+ postcssCustomMediaImportExport(/* pluginOptions */)
+ ];
+
+ return gulp.src('./src/*.css')
+ .pipe(postcss(plugins))
+ .pipe(gulp.dest('.'));
+});
+```
+
+## Grunt
+
+Add [Grunt PostCSS] to your project:
+
+```bash
+npm install grunt-postcss @csstools/postcss-custom-media-import-export --save-dev
+```
+
+Use [PostCSS Custom Media Import/Export] in your Gruntfile:
+
+```js
+const postcssCustomMediaImportExport = require('@csstools/postcss-custom-media-import-export');
+
+grunt.loadNpmTasks('grunt-postcss');
+
+grunt.initConfig({
+ postcss: {
+ options: {
+ processors: [
+ postcssCustomMediaImportExport(/* 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 Import/Export]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-media-import-export
+[React App Rewire PostCSS]: https://github.com/csstools/react-app-rewire-postcss
+[React App Rewired]: https://github.com/timarney/react-app-rewired
+[Next.js]: https://nextjs.org
diff --git a/plugins/postcss-custom-media-import-export/LICENSE.md b/plugins/postcss-custom-media-import-export/LICENSE.md
new file mode 100644
index 000000000..6d7047088
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/LICENSE.md
@@ -0,0 +1,21 @@
+# 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.
diff --git a/plugins/postcss-custom-media-import-export/README.md b/plugins/postcss-custom-media-import-export/README.md
new file mode 100644
index 000000000..7d056e66c
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/README.md
@@ -0,0 +1,162 @@
+# PostCSS Custom Media Import/Export [
][PostCSS]
+
+[
][npm-url] [
][cli-url] [
][discord]
+
+[PostCSS Custom Media Import/Export] lets you import or export `@custom-media`'s into or out of your CSS.
+
+## As a drop in for old versions of `postcss-custom-media`
+
+```js
+// commonjs
+const postcss = require('postcss');
+const postcssCustomMediaImportExport = require('@csstools/postcss-custom-media-import-export');
+const postcssCustomMedia = require('postcss-custom-media');
+
+postcss([
+ // First
+ postcssCustomMediaImportExport({
+ /* pluginOptions */
+ importedStylesOverrideDocumentStyles: false, // mimics old `postcss-custom-media`
+ }),
+ // Second
+ postcssCustomMedia()
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+## Usage
+
+Add [PostCSS Custom Media Import/Export] to your project:
+
+```bash
+npm install postcss @csstools/postcss-custom-media-import-export --save-dev
+```
+
+Use it as a [PostCSS] plugin:
+
+```js
+const postcss = require('postcss');
+const postcssCustomMediaImportExport = require('@csstools/postcss-custom-media-import-export');
+
+postcss([
+ postcssCustomMediaImportExport(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+[PostCSS Custom Media Import/Export] runs in all Node environments, with special
+instructions for:
+
+- [Node](INSTALL.md#node)
+- [PostCSS CLI](INSTALL.md#postcss-cli)
+- [PostCSS Load Config](INSTALL.md#postcss-load-config)
+- [Webpack](INSTALL.md#webpack)
+- [Create React App](INSTALL.md#create-react-app)
+- [Next.js](INSTALL.md#nextjs)
+- [Gulp](INSTALL.md#gulp)
+- [Grunt](INSTALL.md#grunt)
+
+## Options
+
+### 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
+postcssCustomMediaImportExport({
+ importFrom: 'path/to/file.css' // => @custom-selector --small-viewport (max-width: 30em);
+});
+```
+
+```pcss
+@media (--small-viewport) {
+ /* styles for small viewport */
+}
+
+/* becomes */
+
+@custom-selector --small-viewport (max-width: 30em);
+
+@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
+postcssCustomMediaImportExport({
+ 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 };
+ }
+ ]
+});
+```
+
+### importedStylesOverrideDocumentStyles
+
+The `importedStylesOverrideDocumentStyles` option determines if queries added via `importFrom` override queries that exist in your CSS document.
+Defaults to `false`.
+
+```js
+postcssCustomMediaImportExport({
+ importedStylesOverrideDocumentStyles: true
+});
+
+### 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
+postcssCustomMediaImportExport({
+ 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: {} };
+
+postcssCustomMediaImportExport({
+ 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-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test
+
+[discord]: https://discord.gg/bUadyRwkJS
+[npm-url]: https://www.npmjs.com/package/@csstools/postcss-custom-media-import-export
+
+[PostCSS]: https://github.com/postcss/postcss
+[PostCSS Custom Media Import/Export]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-media-import-export
diff --git a/plugins/postcss-custom-media-import-export/docs/README.md b/plugins/postcss-custom-media-import-export/docs/README.md
new file mode 100644
index 000000000..18b649092
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/docs/README.md
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[] lets you import or export `@custom-media`'s into or out of your CSS.
+
+## As a drop in for old versions of `postcss-custom-media`
+
+```js
+// commonjs
+const postcss = require('postcss');
+const postcssCustomMediaImportExport = require('@csstools/postcss-custom-media-import-export');
+const postcssCustomMedia = require('postcss-custom-media');
+
+postcss([
+ // First
+ postcssCustomMediaImportExport({
+ /* pluginOptions */
+ importedStylesOverrideDocumentStyles: false, // mimics old `postcss-custom-media`
+ }),
+ // Second
+ postcssCustomMedia()
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+
+
+
+
+## Options
+
+### 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
+({
+ importFrom: 'path/to/file.css' // => @custom-selector --small-viewport (max-width: 30em);
+});
+```
+
+```pcss
+@media (--small-viewport) {
+ /* styles for small viewport */
+}
+
+/* becomes */
+
+@custom-selector --small-viewport (max-width: 30em);
+
+@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
+({
+ 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 };
+ }
+ ]
+});
+```
+
+### importedStylesOverrideDocumentStyles
+
+The `importedStylesOverrideDocumentStyles` option determines if queries added via `importFrom` override queries that exist in your CSS document.
+Defaults to `false`.
+
+```js
+({
+ importedStylesOverrideDocumentStyles: true
+});
+
+### 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
+({
+ 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: {} };
+
+({
+ 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).
+
+
diff --git a/plugins/postcss-custom-media-import-export/package.json b/plugins/postcss-custom-media-import-export/package.json
new file mode 100644
index 000000000..f72b9e5ab
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/package.json
@@ -0,0 +1,93 @@
+{
+ "name": "@csstools/postcss-custom-media-import-export",
+ "description": "Import/Export Custom Media Queries in CSS",
+ "version": "1.0.0",
+ "contributors": [
+ {
+ "name": "Antonio Laguna",
+ "email": "antonio@laguna.es",
+ "url": "https://antonio.laguna.es"
+ },
+ {
+ "name": "Romain Menke",
+ "email": "romainmenke@gmail.com"
+ },
+ {
+ "name": "Jonathan Neal",
+ "email": "jonathantneal@hotmail.com"
+ },
+ {
+ "name": "Maxime Thirouin"
+ }
+ ],
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "main": "dist/index.cjs",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.cjs",
+ "default": "./dist/index.mjs"
+ }
+ },
+ "files": [
+ "CHANGELOG.md",
+ "LICENSE.md",
+ "README.md",
+ "dist"
+ ],
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ },
+ "devDependencies": {
+ "postcss-custom-media": "^9.0.1"
+ },
+ "scripts": {
+ "build": "rollup -c ../../rollup/default.mjs",
+ "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"",
+ "docs": "node ../../.github/bin/generate-docs/install.mjs && node ../../.github/bin/generate-docs/readme.mjs",
+ "lint": "npm run lint:eslint && npm run lint:package-json",
+ "lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern",
+ "lint:package-json": "node ../../.github/bin/format-package-json.mjs",
+ "prepublishOnly": "npm run clean && npm run build && npm run test",
+ "test": "node .tape.cjs && npm run test:exports",
+ "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs",
+ "test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.cjs"
+ },
+ "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-media-import-export#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/csstools/postcss-plugins.git",
+ "directory": "plugins/postcss-custom-media-import-export"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "at-rule",
+ "atrule",
+ "css",
+ "custom",
+ "media",
+ "postcss",
+ "postcss-plugin",
+ "queries",
+ "query"
+ ],
+ "csstools": {
+ "exportName": "postcssCustomMediaImportExport",
+ "humanReadableName": "PostCSS Custom Media Import/Export"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/src/custom-media-from-root.js b/plugins/postcss-custom-media-import-export/src/custom-media-from-root.js
new file mode 100644
index 000000000..6b64a6607
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/src/custom-media-from-root.js
@@ -0,0 +1,55 @@
+import valueParser from 'postcss-value-parser';
+
+// return custom media from the css root, conditionally removing them
+export default (root) => {
+ // initialize custom media
+ const customMedias = {};
+
+ // for each custom selector atrule that is a child of the css root
+ root.nodes.slice().forEach(node => {
+ if (node.type !== 'atrule') {
+ return;
+ }
+
+ if (node.name.toLowerCase() !== 'custom-media') {
+ return;
+ }
+
+ let paramsAst = null;
+ try {
+ paramsAst = valueParser(node.params);
+ } catch (_) {
+ return;
+ }
+
+ if (!paramsAst || !paramsAst.nodes || !paramsAst.nodes.length) {
+ return;
+ }
+
+ let nameNodeIndex = -1;
+ for (let i = 0; i < paramsAst.nodes.length; i++) {
+ const node = paramsAst.nodes[i];
+ if (node.type === 'space' || node.type === 'comment') {
+ continue;
+ }
+
+ if (node.type === 'word' && node.value.startsWith('--')) {
+ nameNodeIndex = i;
+ break;
+ }
+
+ return; /* invalid starting node */
+ }
+
+ if (nameNodeIndex < 0) {
+ return;
+ }
+
+ const name = paramsAst.nodes[nameNodeIndex].value.trim();
+ const mediaQueries = valueParser.stringify(paramsAst.nodes.slice(nameNodeIndex + 1)).trim();
+
+ customMedias[name] = mediaQueries;
+ });
+
+ return customMedias;
+};
diff --git a/plugins/postcss-custom-media-import-export/src/get-custom-media-from-imports.js b/plugins/postcss-custom-media-import-export/src/get-custom-media-from-imports.js
new file mode 100644
index 000000000..6a43c5739
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/src/get-custom-media-from-imports.js
@@ -0,0 +1,115 @@
+import fs from 'fs';
+import path from 'path';
+import url from 'url';
+import { parse } from 'postcss';
+import getCustomMedia from './custom-media-from-root';
+
+/* Get Custom Media from CSS File
+/* ========================================================================== */
+
+async function getCustomMediaFromCSSFile(from) {
+ const css = await readFile(url.pathToFileURL(path.resolve(from)));
+ const root = parse(css, { from });
+
+ return getCustomMedia(root);
+}
+
+/* Get Custom Media from Object
+/* ========================================================================== */
+
+function getCustomMediaFromObject(object) {
+ const customMedia = Object.assign(
+ {},
+ Object(object).customMedia,
+ Object(object)['custom-media'],
+ );
+
+ return customMedia;
+}
+
+/* Get Custom Media from JSON file
+/* ========================================================================== */
+
+async function getCustomMediaFromJSONFile(from) {
+ const object = await readJSON(url.pathToFileURL(path.resolve(from)));
+
+ return getCustomMediaFromObject(object);
+}
+
+/* Get Custom Media from JS file
+/* ========================================================================== */
+
+async function getCustomMediaFromJSFile(from) {
+ const object = await import(url.pathToFileURL(path.resolve(from)).href);
+
+ if ('default' in object) {
+ return getCustomMediaFromObject(object.default);
+ }
+
+ 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' || type === 'cjs') {
+ return Object.assign(await customMedia, await getCustomMediaFromJSFile(from));
+ }
+
+ if (type === 'mjs') {
+ // Only works when running as a module.
+ 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));
diff --git a/plugins/postcss-custom-media-import-export/src/index.js b/plugins/postcss-custom-media-import-export/src/index.js
new file mode 100644
index 000000000..d63d4f1a9
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/src/index.js
@@ -0,0 +1,69 @@
+import getCustomMediaFromRoot from './custom-media-from-root';
+import getCustomMediaFromImports from './get-custom-media-from-imports';
+import writeCustomMediaToExports from './write-custom-media-to-exports';
+
+const creator = (opts) => {
+ const importedStylesOverrideDocumentStyles = 'importedStylesOverrideDocumentStyles' in Object(opts) ? Boolean(opts.importedStylesOverrideDocumentStyles) : false;
+
+ // sources to import custom media from
+ const importFrom = [].concat(Object(opts).importFrom || []);
+
+ // destinations to export custom selectors to
+ const exportTo = [].concat(Object(opts).exportTo || []);
+
+ // promise any custom media are imported
+ const customMediaImportsPromise = getCustomMediaFromImports(importFrom);
+
+ return {
+ postcssPlugin: 'postcss-custom-media-import-export',
+ Once: async (root, { result, postcss }) => {
+ const importedMediaQueries = await customMediaImportsPromise;
+
+ let allCustomMediaQueries;
+ if (importedStylesOverrideDocumentStyles) {
+ allCustomMediaQueries = Object.assign(
+ {},
+ getCustomMediaFromRoot(root),
+ importedMediaQueries,
+ );
+ } else {
+ allCustomMediaQueries = Object.assign(
+ {},
+ importedMediaQueries,
+ getCustomMediaFromRoot(root),
+ );
+ }
+
+ await writeCustomMediaToExports(allCustomMediaQueries, exportTo);
+
+ if (importedMediaQueries) {
+ const mediaQueryNames = Object.keys(importedMediaQueries);
+ // Inserting in reverse order results in the correct order.
+ mediaQueryNames.reverse();
+
+ let operator = 'prepend';
+ if (importedStylesOverrideDocumentStyles) {
+ operator = 'append';
+ }
+
+ mediaQueryNames.forEach((mediaQueryName) => {
+ root[operator](postcss.atRule({
+ name: 'custom-media',
+ params: `${mediaQueryName} ${importedMediaQueries[mediaQueryName].toString()}`,
+ source: {
+ input: {
+ from: result.opts.from,
+ },
+ start: { offset: 0, line: 1, column: 1 },
+ end: { offset: 0, line: 1, column: 1 },
+ },
+ }));
+ });
+ }
+ },
+ };
+};
+
+creator.postcss = true;
+
+export default creator;
diff --git a/plugins/postcss-custom-media-import-export/src/write-custom-media-to-exports.js b/plugins/postcss-custom-media-import-export/src/write-custom-media-to-exports.js
new file mode 100644
index 000000000..52754f3f5
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/src/write-custom-media-to-exports.js
@@ -0,0 +1,129 @@
+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, '\t');
+ 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');
diff --git a/plugins/postcss-custom-media-import-export/test/_import.mjs b/plugins/postcss-custom-media-import-export/test/_import.mjs
new file mode 100644
index 000000000..0b023a196
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/_import.mjs
@@ -0,0 +1,6 @@
+import assert from 'assert';
+import plugin from '@csstools/postcss-custom-media-import-export';
+plugin();
+
+assert.ok(plugin.postcss, 'should have "postcss flag"');
+assert.equal(typeof plugin, 'function', 'should return a function');
diff --git a/plugins/postcss-custom-media-import-export/test/_require.cjs b/plugins/postcss-custom-media-import-export/test/_require.cjs
new file mode 100644
index 000000000..8af6094ba
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/_require.cjs
@@ -0,0 +1,6 @@
+const assert = require('assert');
+const plugin = require('@csstools/postcss-custom-media-import-export');
+plugin();
+
+assert.ok(plugin.postcss, 'should have "postcss flag"');
+assert.equal(typeof plugin, 'function', 'should return a function');
diff --git a/plugins/postcss-custom-media-import-export/test/basic.css b/plugins/postcss-custom-media-import-export/test/basic.css
new file mode 100644
index 000000000..a992cfe2f
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/basic.css
@@ -0,0 +1,132 @@
+@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;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1000;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1001;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1002;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1003;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1004;
+ }
+}
+
+@media (
+ --mq-a
+),
+(
+ --mq-a
+) {
+ body {
+ order: 1005;
+ }
+}
+
+@media (trailer--) {
+ body {
+ order: 1006;
+ }
+}
+
+@custom-media trailer-- (min-width: 320px);
diff --git a/plugins/postcss-custom-media-import-export/test/basic.export-css-to-type.expect.css b/plugins/postcss-custom-media-import-export/test/basic.export-css-to-type.expect.css
new file mode 100644
index 000000000..a992cfe2f
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/basic.export-css-to-type.expect.css
@@ -0,0 +1,132 @@
+@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;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1000;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1001;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1002;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1003;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1004;
+ }
+}
+
+@media (
+ --mq-a
+),
+(
+ --mq-a
+) {
+ body {
+ order: 1005;
+ }
+}
+
+@media (trailer--) {
+ body {
+ order: 1006;
+ }
+}
+
+@custom-media trailer-- (min-width: 320px);
diff --git a/plugins/postcss-custom-media-import-export/test/basic.export-css-to.expect.css b/plugins/postcss-custom-media-import-export/test/basic.export-css-to.expect.css
new file mode 100644
index 000000000..a992cfe2f
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/basic.export-css-to.expect.css
@@ -0,0 +1,132 @@
+@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;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1000;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1001;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1002;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1003;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1004;
+ }
+}
+
+@media (
+ --mq-a
+),
+(
+ --mq-a
+) {
+ body {
+ order: 1005;
+ }
+}
+
+@media (trailer--) {
+ body {
+ order: 1006;
+ }
+}
+
+@custom-media trailer-- (min-width: 320px);
diff --git a/plugins/postcss-custom-media-import-export/test/basic.export-css.expect.css b/plugins/postcss-custom-media-import-export/test/basic.export-css.expect.css
new file mode 100644
index 000000000..a992cfe2f
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/basic.export-css.expect.css
@@ -0,0 +1,132 @@
+@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;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1000;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1001;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1002;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1003;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1004;
+ }
+}
+
+@media (
+ --mq-a
+),
+(
+ --mq-a
+) {
+ body {
+ order: 1005;
+ }
+}
+
+@media (trailer--) {
+ body {
+ order: 1006;
+ }
+}
+
+@custom-media trailer-- (min-width: 320px);
diff --git a/plugins/postcss-custom-media-import-export/test/basic.export-fn-promise.expect.css b/plugins/postcss-custom-media-import-export/test/basic.export-fn-promise.expect.css
new file mode 100644
index 000000000..a992cfe2f
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/basic.export-fn-promise.expect.css
@@ -0,0 +1,132 @@
+@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;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1000;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1001;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1002;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1003;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1004;
+ }
+}
+
+@media (
+ --mq-a
+),
+(
+ --mq-a
+) {
+ body {
+ order: 1005;
+ }
+}
+
+@media (trailer--) {
+ body {
+ order: 1006;
+ }
+}
+
+@custom-media trailer-- (min-width: 320px);
diff --git a/plugins/postcss-custom-media-import-export/test/basic.export-fn.expect.css b/plugins/postcss-custom-media-import-export/test/basic.export-fn.expect.css
new file mode 100644
index 000000000..a992cfe2f
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/basic.export-fn.expect.css
@@ -0,0 +1,132 @@
+@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;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1000;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1001;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1002;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1003;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1004;
+ }
+}
+
+@media (
+ --mq-a
+),
+(
+ --mq-a
+) {
+ body {
+ order: 1005;
+ }
+}
+
+@media (trailer--) {
+ body {
+ order: 1006;
+ }
+}
+
+@custom-media trailer-- (min-width: 320px);
diff --git a/plugins/postcss-custom-media-import-export/test/basic.export-js.expect.css b/plugins/postcss-custom-media-import-export/test/basic.export-js.expect.css
new file mode 100644
index 000000000..a992cfe2f
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/basic.export-js.expect.css
@@ -0,0 +1,132 @@
+@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;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1000;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1001;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1002;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1003;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1004;
+ }
+}
+
+@media (
+ --mq-a
+),
+(
+ --mq-a
+) {
+ body {
+ order: 1005;
+ }
+}
+
+@media (trailer--) {
+ body {
+ order: 1006;
+ }
+}
+
+@custom-media trailer-- (min-width: 320px);
diff --git a/plugins/postcss-custom-media-import-export/test/basic.export-json.expect.css b/plugins/postcss-custom-media-import-export/test/basic.export-json.expect.css
new file mode 100644
index 000000000..a992cfe2f
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/basic.export-json.expect.css
@@ -0,0 +1,132 @@
+@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;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1000;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1001;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1002;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1003;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1004;
+ }
+}
+
+@media (
+ --mq-a
+),
+(
+ --mq-a
+) {
+ body {
+ order: 1005;
+ }
+}
+
+@media (trailer--) {
+ body {
+ order: 1006;
+ }
+}
+
+@custom-media trailer-- (min-width: 320px);
diff --git a/plugins/postcss-custom-media-import-export/test/basic.export-mjs.expect.css b/plugins/postcss-custom-media-import-export/test/basic.export-mjs.expect.css
new file mode 100644
index 000000000..a992cfe2f
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/basic.export-mjs.expect.css
@@ -0,0 +1,132 @@
+@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;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1000;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1001;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1002;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1003;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1004;
+ }
+}
+
+@media (
+ --mq-a
+),
+(
+ --mq-a
+) {
+ body {
+ order: 1005;
+ }
+}
+
+@media (trailer--) {
+ body {
+ order: 1006;
+ }
+}
+
+@custom-media trailer-- (min-width: 320px);
diff --git a/plugins/postcss-custom-media-import-export/test/basic.export.expect.css b/plugins/postcss-custom-media-import-export/test/basic.export.expect.css
new file mode 100644
index 000000000..a992cfe2f
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/basic.export.expect.css
@@ -0,0 +1,132 @@
+@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;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1000;
+ }
+}
+
+@media ( --mq-a ) {
+ body {
+ order: 1001;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1002;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1003;
+ }
+}
+
+@media ( --mq-a ), ( --mq-a ) {
+ body {
+ order: 1004;
+ }
+}
+
+@media (
+ --mq-a
+),
+(
+ --mq-a
+) {
+ body {
+ order: 1005;
+ }
+}
+
+@media (trailer--) {
+ body {
+ order: 1006;
+ }
+}
+
+@custom-media trailer-- (min-width: 320px);
diff --git a/plugins/postcss-custom-media-import-export/test/basic.import.expect.css b/plugins/postcss-custom-media-import-export/test/basic.import.expect.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/plugins/postcss-custom-media-import-export/test/examples/.gitkeep b/plugins/postcss-custom-media-import-export/test/examples/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/plugins/postcss-custom-media-import-export/test/export-media.css b/plugins/postcss-custom-media-import-export/test/export-media.css
new file mode 100644
index 000000000..f51e88c82
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/export-media.css
@@ -0,0 +1,8 @@
+@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);
diff --git a/plugins/postcss-custom-media-import-export/test/export-media.js b/plugins/postcss-custom-media-import-export/test/export-media.js
new file mode 100644
index 000000000..acccd8408
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/export-media.js
@@ -0,0 +1,12 @@
+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)'
+ }
+};
diff --git a/plugins/postcss-custom-media-import-export/test/export-media.json b/plugins/postcss-custom-media-import-export/test/export-media.json
new file mode 100644
index 000000000..729bde28e
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/export-media.json
@@ -0,0 +1,12 @@
+{
+ "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)"
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/export-media.mjs b/plugins/postcss-custom-media-import-export/test/export-media.mjs
new file mode 100644
index 000000000..ea36519d8
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/export-media.mjs
@@ -0,0 +1,10 @@
+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)'
+};
diff --git a/plugins/postcss-custom-media-import-export/test/import-css.css b/plugins/postcss-custom-media-import-export/test/import-css.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/plugins/postcss-custom-media-import-export/test/import-media.css b/plugins/postcss-custom-media-import-export/test/import-media.css
new file mode 100644
index 000000000..e788f32ae
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import-media.css
@@ -0,0 +1,2 @@
+@custom-media --mq-a (max-width: 30em), (max-height: 30em);
+@custom-media --not-mq-a not all and (--mq-a);
diff --git a/plugins/postcss-custom-media-import-export/test/import-media.js b/plugins/postcss-custom-media-import-export/test/import-media.js
new file mode 100644
index 000000000..3f2e0401a
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import-media.js
@@ -0,0 +1,6 @@
+module.exports = {
+ customMedia: {
+ '--mq-a': '(max-width: 30em), (max-height: 30em)',
+ '--not-mq-a': 'not all and (--mq-a)'
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import-media.json b/plugins/postcss-custom-media-import-export/test/import-media.json
new file mode 100644
index 000000000..807d8dfdd
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import-media.json
@@ -0,0 +1,6 @@
+{
+ "customMedia": {
+ "--mq-a": "(max-width: 30em), (max-height: 30em)",
+ "--not-mq-a": "not all and (--mq-a)"
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.css b/plugins/postcss-custom-media-import-export/test/import.css
new file mode 100644
index 000000000..f37220e74
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.css
@@ -0,0 +1,29 @@
+@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;
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.css-from-type.expect.css b/plugins/postcss-custom-media-import-export/test/import.css-from-type.expect.css
new file mode 100644
index 000000000..6bbae6719
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.css-from-type.expect.css
@@ -0,0 +1,33 @@
+@custom-media --mq-a (max-width: 30em), (max-height: 30em);
+
+@custom-media --not-mq-a not all and (--mq-a);
+
+@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;
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.css-from.expect.css b/plugins/postcss-custom-media-import-export/test/import.css-from.expect.css
new file mode 100644
index 000000000..6bbae6719
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.css-from.expect.css
@@ -0,0 +1,33 @@
+@custom-media --mq-a (max-width: 30em), (max-height: 30em);
+
+@custom-media --not-mq-a not all and (--mq-a);
+
+@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;
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.css.expect.css b/plugins/postcss-custom-media-import-export/test/import.css.expect.css
new file mode 100644
index 000000000..6bbae6719
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.css.expect.css
@@ -0,0 +1,33 @@
+@custom-media --mq-a (max-width: 30em), (max-height: 30em);
+
+@custom-media --not-mq-a not all and (--mq-a);
+
+@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;
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.empty.expect.css b/plugins/postcss-custom-media-import-export/test/import.empty.expect.css
new file mode 100644
index 000000000..f37220e74
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.empty.expect.css
@@ -0,0 +1,29 @@
+@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;
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.expect.css b/plugins/postcss-custom-media-import-export/test/import.expect.css
new file mode 100644
index 000000000..6bbae6719
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.expect.css
@@ -0,0 +1,33 @@
+@custom-media --mq-a (max-width: 30em), (max-height: 30em);
+
+@custom-media --not-mq-a not all and (--mq-a);
+
+@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;
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.import-fn-promise.expect.css b/plugins/postcss-custom-media-import-export/test/import.import-fn-promise.expect.css
new file mode 100644
index 000000000..6bbae6719
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.import-fn-promise.expect.css
@@ -0,0 +1,33 @@
+@custom-media --mq-a (max-width: 30em), (max-height: 30em);
+
+@custom-media --not-mq-a not all and (--mq-a);
+
+@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;
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.import-fn.expect.css b/plugins/postcss-custom-media-import-export/test/import.import-fn.expect.css
new file mode 100644
index 000000000..6bbae6719
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.import-fn.expect.css
@@ -0,0 +1,33 @@
+@custom-media --mq-a (max-width: 30em), (max-height: 30em);
+
+@custom-media --not-mq-a not all and (--mq-a);
+
+@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;
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.imported-styles-override-document-styles.false.expect.css b/plugins/postcss-custom-media-import-export/test/import.imported-styles-override-document-styles.false.expect.css
new file mode 100644
index 000000000..6bbae6719
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.imported-styles-override-document-styles.false.expect.css
@@ -0,0 +1,33 @@
+@custom-media --mq-a (max-width: 30em), (max-height: 30em);
+
+@custom-media --not-mq-a not all and (--mq-a);
+
+@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;
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.imported-styles-override-document-styles.true.expect.css b/plugins/postcss-custom-media-import-export/test/import.imported-styles-override-document-styles.true.expect.css
new file mode 100644
index 000000000..5b32dc079
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.imported-styles-override-document-styles.true.expect.css
@@ -0,0 +1,33 @@
+@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;
+ }
+}
+
+@custom-media --not-mq-a not all and (--mq-a);
+
+@custom-media --mq-a (max-width: 30em), (max-height: 30em)
diff --git a/plugins/postcss-custom-media-import-export/test/import.js.expect.css b/plugins/postcss-custom-media-import-export/test/import.js.expect.css
new file mode 100644
index 000000000..6bbae6719
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.js.expect.css
@@ -0,0 +1,33 @@
+@custom-media --mq-a (max-width: 30em), (max-height: 30em);
+
+@custom-media --not-mq-a not all and (--mq-a);
+
+@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;
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.json.expect.css b/plugins/postcss-custom-media-import-export/test/import.json.expect.css
new file mode 100644
index 000000000..6bbae6719
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.json.expect.css
@@ -0,0 +1,33 @@
+@custom-media --mq-a (max-width: 30em), (max-height: 30em);
+
+@custom-media --not-mq-a not all and (--mq-a);
+
+@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;
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.plugin.expect.css b/plugins/postcss-custom-media-import-export/test/import.plugin.expect.css
new file mode 100644
index 000000000..0bc2bbf3b
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.plugin.expect.css
@@ -0,0 +1,29 @@
+@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;
+ }
+}
diff --git a/plugins/postcss-custom-media-import-export/test/import.with-polyfill-plugin.expect.css b/plugins/postcss-custom-media-import-export/test/import.with-polyfill-plugin.expect.css
new file mode 100644
index 000000000..e28c85d2a
--- /dev/null
+++ b/plugins/postcss-custom-media-import-export/test/import.with-polyfill-plugin.expect.css
@@ -0,0 +1,95 @@
+@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 (max-width: 30em),(max-height: 30em) {
+
+@media not all and (max-color:2147477350) {
+ body {
+ order: 2;
+ }
+}
+}
+
+@media not all and (max-width: 30em),not all and (max-height: 30em) {
+
+@media not all and (color:2147477350) {
+ body {
+ order: 2;
+ }
+}
+}
+
+@media (max-width: 30em),(max-height: 30em) {
+
+@media not all and (max-color:2147477350) {
+ body {
+ order: 1;
+ }
+}
+}
+
+@media not all and (max-width: 30em),not all and (max-height: 30em) {
+
+@media not all and (color:2147477350) {
+ body {
+ order: 1;
+ }
+}
+}
+
+@media (max-width: 30em),(max-height: 30em) {
+
+@media not all and (max-color:2147477350) {
+
+@media not all and (max-color:2147477350) {
+ body {
+ order: 2;
+ }
+}
+}
+}
+
+@media not all and (max-width: 30em),not all and (max-height: 30em) {
+
+@media not all and (color:2147477350) {
+
+@media not all and (max-color:2147477350) {
+ body {
+ order: 2;
+ }
+}
+}
+}
+
+@media (max-width: 30em),(max-height: 30em) {
+
+@media all and (max-color:2147477350) {
+
+@media not all and (color:2147477350) {
+ body {
+ order: 2;
+ }
+}
+}
+}
+
+@media not all and (max-width: 30em),not all and (max-height: 30em) {
+
+@media all and (color:2147477350) {
+
+@media not all and (color:2147477350) {
+ body {
+ order: 2;
+ }
+}
+}
+}
diff --git a/plugins/postcss-custom-properties-import-export/.gitignore b/plugins/postcss-custom-properties-import-export/.gitignore
new file mode 100644
index 000000000..e5835f4f9
--- /dev/null
+++ b/plugins/postcss-custom-properties-import-export/.gitignore
@@ -0,0 +1,14 @@
+node_modules
+dist
+package-lock.json
+yarn.lock
+browser.js
+browser.min.js
+*.log*
+*.result.css
+*.result.css.map
+!.editorconfig
+!.gitignore
+!.rollup.js
+!.tape.js
+!.travis.yml
diff --git a/plugins/postcss-custom-properties-import-export/.nvmrc b/plugins/postcss-custom-properties-import-export/.nvmrc
new file mode 100644
index 000000000..f0b10f153
--- /dev/null
+++ b/plugins/postcss-custom-properties-import-export/.nvmrc
@@ -0,0 +1 @@
+v16.13.1
diff --git a/plugins/postcss-custom-properties-import-export/.tape.cjs b/plugins/postcss-custom-properties-import-export/.tape.cjs
new file mode 100644
index 000000000..166d4f387
--- /dev/null
+++ b/plugins/postcss-custom-properties-import-export/.tape.cjs
@@ -0,0 +1,23 @@
+const postcssTape = require('../../packages/postcss-tape/dist/index.cjs');
+const plugin = require('@csstools/postcss-custom-properties-import-export');
+
+postcssTape(plugin)({
+ 'basic:import-cjs': {
+ message: 'supports { importFrom: "test/import-properties{-2}?.cjs" } usage',
+ options: {
+ importFrom: [
+ 'test/import-properties.cjs',
+ 'test/import-properties-2.cjs'
+ ]
+ },
+ },
+ 'basic:import-css-js': {
+ message: 'supports { importFrom: "test/import-properties{-2}?.{css|js}" } usage',
+ options: {
+ importFrom: [
+ 'test/import-properties.js',
+ 'test/import-properties-2.css'
+ ]
+ },
+ }
+});
diff --git a/plugins/postcss-custom-properties-import-export/.tape.mjs b/plugins/postcss-custom-properties-import-export/.tape.mjs
new file mode 100644
index 000000000..09d68a303
--- /dev/null
+++ b/plugins/postcss-custom-properties-import-export/.tape.mjs
@@ -0,0 +1,372 @@
+import postcssTape from '../../packages/postcss-tape/dist/index.mjs';
+import plugin from '@csstools/postcss-custom-properties-import-export';
+import polyfillPlugin from 'postcss-custom-properties';
+import { strict as assert } from 'assert';
+import postcssImport from 'postcss-import';
+import fs from 'fs';
+
+postcssTape(plugin)({
+ 'basic:import': {
+ message: 'supports { importFrom: { customProperties: { ... } } } usage',
+ options: {
+ importFrom: {
+ customProperties: {
+ '--color': 'rgb(255, 0, 0)',
+ '--color-2': 'yellow',
+ '--ref-color': 'var(--color)',
+ '--margin': '0 10px 20px 30px',
+ '--z-index': 10
+ }
+ }
+ }
+ },
+ 'basic:import-with-polyfill-plugin': {
+ message: 'works correctly together with the polyfill',
+ plugins: [
+ plugin({
+ importFrom: {
+ customProperties: {
+ '--color': 'rgb(255, 0, 0)',
+ '--color-2': 'yellow',
+ '--ref-color': 'var(--color)',
+ '--margin': '0 10px 20px 30px',
+ '--z-index': 10
+ }
+ }
+ }),
+ polyfillPlugin(),
+ ]
+ },
+ 'basic:import-imported-styles-override-document-styles:true': {
+ message: 'supports { importedStylesOverrideDocumentStyles: true } usage',
+ options: {
+ importedStylesOverrideDocumentStyles: true,
+ importFrom: {
+ customProperties: {
+ '--color': 'rgb(255, 0, 0)',
+ '--color-2': 'yellow',
+ '--ref-color': 'var(--color)',
+ '--margin': '0 10px 20px 30px',
+ '--z-index': 10
+ }
+ }
+ }
+ },
+ 'basic:import-imported-styles-override-document-styles:false': {
+ message: 'supports { importedStylesOverrideDocumentStyles: false } usage',
+ options: {
+ importedStylesOverrideDocumentStyles: false,
+ importFrom: {
+ customProperties: {
+ '--color': 'rgb(255, 0, 0)',
+ '--color-2': 'yellow',
+ '--ref-color': 'var(--color)',
+ '--margin': '0 10px 20px 30px',
+ '--z-index': 10
+ }
+ }
+ }
+ },
+ 'basic:import-fn': {
+ message: 'supports { importFrom() } usage',
+ options: {
+ importFrom() {
+ return {
+ customProperties: {
+ '--color': 'rgb(255, 0, 0)',
+ '--color-2': 'yellow',
+ '--ref-color': 'var(--color)',
+ '--margin': '0 10px 20px 30px',
+ '--z-index': 10
+ }
+ };
+ }
+ },
+ },
+ 'basic:import-fn-promise': {
+ message: 'supports { async importFrom() } usage',
+ options: {
+ importFrom() {
+ return new Promise(resolve => {
+ resolve({
+ customProperties: {
+ '--color': 'rgb(255, 0, 0)',
+ '--color-2': 'yellow',
+ '--ref-color': 'var(--color)',
+ '--z-index': 10
+ }
+ })
+ });
+ }
+ },
+ },
+ 'basic:import-json': {
+ message: 'supports { importFrom: "test/import-properties.json" } usage',
+ options: {
+ importFrom: 'test/import-properties.json'
+ },
+ },
+ 'basic:import-cjs': {
+ message: 'supports { importFrom: "test/import-properties{-2}?.cjs" } usage',
+ options: {
+ importFrom: [
+ 'test/import-properties.cjs',
+ 'test/import-properties-2.cjs'
+ ]
+ },
+ },
+ 'basic:import-mjs': {
+ message: 'supports { importFrom: "test/import-properties{-2}?.mjs" } usage',
+ options: {
+ importFrom: [
+ 'test/import-properties.mjs',
+ 'test/import-properties-2.mjs'
+ ]
+ },
+ },
+ 'basic:import-css': {
+ message: 'supports { importFrom: "test/import-properties{-2}?.css" } usage',
+ options: {
+ importFrom: [
+ 'test/import-properties.css',
+ 'test/import-properties-2.css'
+ ]
+ },
+ },
+ 'basic:import-css-js': {
+ message: 'supports { importFrom: "test/import-properties{-2}?.{css|js}" } usage',
+ options: {
+ importFrom: [
+ 'test/import-properties.js',
+ 'test/import-properties-2.css'
+ ]
+ },
+ },
+ 'basic:import-css-pcss': {
+ message: 'supports { importFrom: "test/import-properties.{p}?css" } usage',
+ options: {
+ importFrom: [
+ 'test/import-properties.pcss',
+ 'test/import-properties-2.css'
+ ]
+ },
+ },
+ 'basic:import-css-from': {
+ message: 'supports { importFrom: { from: "test/import-properties.css" } } usage',
+ options: {
+ importFrom: [
+ { from: 'test/import-properties.css' },
+ { from: 'test/import-properties-2.css' }
+ ]
+ },
+ },
+ 'basic:import-css-from-type': {
+ message: 'supports { importFrom: [ { from: "test/import-properties.css", type: "css" } ] } usage',
+ options: {
+ importFrom: [
+ { from: 'test/import-properties.css', type: 'css' },
+ { from: 'test/import-properties-2.css', type: 'css' }
+ ]
+ },
+ },
+ 'basic:import-override': {
+ message: 'importFrom with { preserve: false } should override importFrom properties',
+ options: {
+ preserve: false,
+ importFrom: {
+ customProperties: {
+ '--color': 'rgb(0, 0, 0)',
+ '--color-2': 'yellow',
+ '--ref-color': 'var(--color)',
+ '--margin': '0 10px 20px 30px',
+ '--shadow-color': 'rgb(0,0,0)',
+ '--z-index': 10
+ }
+ }
+ },
+ },
+ 'basic:import-override:inverse': {
+ message: 'importFrom with { preserve: false, importedStylesOverrideDocumentStyles: true } should override root properties',
+ options: {
+ preserve: false,
+ importedStylesOverrideDocumentStyles: true,
+ importFrom: {
+ customProperties: {
+ '--color': 'rgb(0, 0, 0)',
+ '--color-2': 'yellow',
+ '--ref-color': 'var(--color)',
+ '--margin': '0 10px 20px 30px',
+ '--shadow-color': 'rgb(0,0,0)',
+ '--z-index': 10
+ }
+ }
+ },
+ },
+ 'basic:export': {
+ message: 'supports { exportTo: { customProperties: { ... } } } usage',
+ options: {
+ exportTo: (global.__exportPropertiesObject = global.__exportPropertiesObject || {
+ customProperties: null
+ })
+ },
+ after() {
+ if (__exportPropertiesObject.customProperties['--color'] !== 'rgb(255, 0, 0)') {
+ throw new Error('The exportTo function failed');
+ }
+ }
+ },
+ 'basic:export-fn': {
+ message: 'supports { exportTo() } usage',
+ options: {
+ exportTo(customProperties) {
+ if (customProperties['--color'] !== 'rgb(255, 0, 0)') {
+ throw new Error('The exportTo function failed');
+ }
+ }
+ },
+ },
+ 'basic:export-fn-promise': {
+ message: 'supports { async exportTo() } usage',
+ options: {
+ exportTo(customProperties) {
+ return new Promise((resolve, reject) => {
+ if (customProperties['--color'] !== 'rgb(255, 0, 0)') {
+ reject('The exportTo function failed');
+ } else {
+ resolve();
+ }
+ });
+ }
+ },
+ },
+ 'basic:export-scss': {
+ message: 'supports { exportTo: "test/export-properties.scss" } usage',
+ options: {
+ exportTo: 'test/export-properties.scss'
+ },
+ before() {
+ try {
+ global.__exportPropertiesString = fs.readFileSync('test/export-properties.scss', 'utf8');
+ fs.rmSync('test/export-properties.scss');
+ } catch (_) {
+ // ignore
+ }
+ },
+ after() {
+ assert.strictEqual(global.__exportPropertiesString, fs.readFileSync('test/export-properties.scss', 'utf8'));
+ }
+ },
+ 'basic:export-json': {
+ message: 'supports { exportTo: "test/export-properties.json" } usage',
+ options: {
+ exportTo: 'test/export-properties.json'
+ },
+ before() {
+ try {
+ global.__exportPropertiesString = fs.readFileSync('test/export-properties.json', 'utf8');
+ fs.rmSync('test/export-properties.json');
+ } catch (_) {
+ // ignore
+ }
+ },
+ after() {
+ assert.strictEqual(global.__exportPropertiesString, fs.readFileSync('test/export-properties.json', 'utf8'));
+ }
+ },
+ 'basic:export-js': {
+ message: 'supports { exportTo: "test/export-properties.js" } usage',
+ options: {
+ exportTo: 'test/export-properties.js'
+ },
+ before() {
+ try {
+ global.__exportPropertiesString = fs.readFileSync('test/export-properties.js', 'utf8');
+ fs.rmSync('test/export-properties.js');
+ } catch (_) {
+ // ignore
+ }
+ },
+ after() {
+ assert.strictEqual(global.__exportPropertiesString, fs.readFileSync('test/export-properties.js', 'utf8'));
+ }
+ },
+ 'basic:export-mjs': {
+ message: 'supports { exportTo: "test/export-properties.mjs" } usage',
+ options: {
+ exportTo: 'test/export-properties.mjs'
+ },
+ before() {
+ try {
+ global.__exportPropertiesString = fs.readFileSync('test/export-properties.mjs', 'utf8');
+ fs.rmSync('test/export-properties.mjs');
+ } catch (_) {
+ // ignore
+ }
+ },
+ after() {
+ assert.strictEqual(global.__exportPropertiesString, fs.readFileSync('test/export-properties.mjs', 'utf8'));
+ }
+ },
+ 'basic:export-css': {
+ message: 'supports { exportTo: "test/export-properties.css" } usage',
+ options: {
+ exportTo: 'test/export-properties.css'
+ },
+ before() {
+ try {
+ global.__exportPropertiesString = fs.readFileSync('test/export-properties.css', 'utf8');
+ fs.rmSync('test/export-properties.css');
+ } catch (_) {
+ // ignore
+ }
+ },
+ after() {
+ assert.strictEqual(global.__exportPropertiesString, fs.readFileSync('test/export-properties.css', 'utf8'));
+ }
+ },
+ 'basic:export-css-to': {
+ message: 'supports { exportTo: { to: "test/export-properties.css" } } usage',
+ options: {
+ exportTo: { to: 'test/export-properties.css' }
+ },
+ before() {
+ try {
+ global.__exportPropertiesString = fs.readFileSync('test/export-properties.css', 'utf8');
+ fs.rmSync('test/export-properties.css');
+ } catch (_) {
+ // ignore
+ }
+ },
+ after() {
+ assert.strictEqual(global.__exportPropertiesString, fs.readFileSync('test/export-properties.css', 'utf8'));
+ }
+ },
+ 'basic:export-css-to-type': {
+ message: 'supports { exportTo: { to: "test/export-properties.css", type: "css" } } usage',
+ options: {
+ exportTo: { to: 'test/export-properties.css', type: 'css' }
+ },
+ before() {
+ try {
+ global.__exportPropertiesString = fs.readFileSync('test/export-properties.css', 'utf8');
+ fs.rmSync('test/export-properties.css');
+ } catch (_) {
+ // ignore
+ }
+ },
+ after() {
+ assert.strictEqual(global.__exportPropertiesString, fs.readFileSync('test/export-properties.css', 'utf8'));
+ }
+ },
+ 'basic:import-is-empty': {
+ message: 'supports { importFrom: {} } usage',
+ options: {
+ importFrom: {},
+ disableDeprecationNotice: true
+ }
+ },
+ 'import': {
+ message: 'supports "postcss-import"',
+ plugins: [postcssImport(), plugin()]
+ }
+});
diff --git a/plugins/postcss-custom-properties-import-export/CHANGELOG.md b/plugins/postcss-custom-properties-import-export/CHANGELOG.md
new file mode 100644
index 000000000..df73cd762
--- /dev/null
+++ b/plugins/postcss-custom-properties-import-export/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changes to PostCSS Custom Properties Import/Export
+
+### 1.0.0 (Unreleased)
+
+- Initial version
diff --git a/plugins/postcss-custom-properties-import-export/INSTALL.md b/plugins/postcss-custom-properties-import-export/INSTALL.md
new file mode 100644
index 000000000..b215f4bbf
--- /dev/null
+++ b/plugins/postcss-custom-properties-import-export/INSTALL.md
@@ -0,0 +1,257 @@
+# Installing PostCSS Custom Properties Import/Export
+
+[PostCSS Custom Properties Import/Export] runs in all Node environments, with special instructions for:
+
+- [Node](#node)
+- [PostCSS CLI](#postcss-cli)
+- [PostCSS Load Config](#postcss-load-config)
+- [Webpack](#webpack)
+- [Create React App](#create-react-app)
+- [Next.js](#nextjs)
+- [Gulp](#gulp)
+- [Grunt](#grunt)
+
+## Node
+
+Add [PostCSS Custom Properties Import/Export] to your project:
+
+```bash
+npm install postcss @csstools/postcss-custom-properties-import-export --save-dev
+```
+
+Use it as a [PostCSS] plugin:
+
+```js
+// commonjs
+const postcss = require('postcss');
+const postcssCustomPropertiesImportExport = require('@csstools/postcss-custom-properties-import-export');
+
+postcss([
+ postcssCustomPropertiesImportExport(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+```js
+// esm
+import postcss from 'postcss';
+import postcssCustomPropertiesImportExport from '@csstools/postcss-custom-properties-import-export';
+
+postcss([
+ postcssCustomPropertiesImportExport(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+## PostCSS CLI
+
+Add [PostCSS CLI] to your project:
+
+```bash
+npm install postcss-cli @csstools/postcss-custom-properties-import-export --save-dev
+```
+
+Use [PostCSS Custom Properties Import/Export] in your `postcss.config.js` configuration file:
+
+```js
+const postcssCustomPropertiesImportExport = require('@csstools/postcss-custom-properties-import-export');
+
+module.exports = {
+ plugins: [
+ postcssCustomPropertiesImportExport(/* pluginOptions */)
+ ]
+}
+```
+
+## PostCSS Load Config
+
+If your framework/CLI supports [`postcss-load-config`](https://github.com/postcss/postcss-load-config).
+
+```bash
+npm install @csstools/postcss-custom-properties-import-export --save-dev
+```
+
+`package.json`:
+
+```json
+{
+ "postcss": {
+ "plugins": {
+ "@csstools/postcss-custom-properties-import-export": {}
+ }
+ }
+}
+```
+
+`.postcssrc.json`:
+
+```json
+{
+ "plugins": {
+ "@csstools/postcss-custom-properties-import-export": {}
+ }
+}
+```
+
+_See the [README of `postcss-load-config`](https://github.com/postcss/postcss-load-config#usage) for more usage options._
+
+## Webpack
+
+_Webpack version 5_
+
+Add [PostCSS Loader] to your project:
+
+```bash
+npm install postcss-loader @csstools/postcss-custom-properties-import-export --save-dev
+```
+
+Use [PostCSS Custom Properties Import/Export] in your Webpack configuration:
+
+```js
+module.exports = {
+ module: {
+ rules: [
+ {
+ test: /\.css$/i,
+ use: [
+ "style-loader",
+ {
+ loader: "css-loader",
+ options: { importLoaders: 1 },
+ },
+ {
+ loader: "postcss-loader",
+ options: {
+ postcssOptions: {
+ plugins: [
+ [
+ "@csstools/postcss-custom-properties-import-export",
+ {
+ // Options
+ },
+ ],
+ ],
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+};
+```
+
+## 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 @csstools/postcss-custom-properties-import-export --save-dev
+```
+
+Use [React App Rewire PostCSS] and [PostCSS Custom Properties Import/Export] in your
+`config-overrides.js` file:
+
+```js
+const reactAppRewirePostcss = require('react-app-rewire-postcss');
+const postcssCustomPropertiesImportExport = require('@csstools/postcss-custom-properties-import-export');
+
+module.exports = config => reactAppRewirePostcss(config, {
+ plugins: () => [
+ postcssCustomPropertiesImportExport(/* pluginOptions */)
+ ]
+});
+```
+
+## Next.js
+
+Read the instructions on how to [customize the PostCSS configuration in Next.js](https://nextjs.org/docs/advanced-features/customizing-postcss-config)
+
+```bash
+npm install @csstools/postcss-custom-properties-import-export --save-dev
+```
+
+Use [PostCSS Custom Properties Import/Export] in your `postcss.config.json` file:
+
+```json
+{
+ "plugins": [
+ "@csstools/postcss-custom-properties-import-export"
+ ]
+}
+```
+
+```json5
+{
+ "plugins": [
+ [
+ "@csstools/postcss-custom-properties-import-export",
+ {
+ // Optionally add plugin options
+ }
+ ]
+ ]
+}
+```
+
+## Gulp
+
+Add [Gulp PostCSS] to your project:
+
+```bash
+npm install gulp-postcss @csstools/postcss-custom-properties-import-export --save-dev
+```
+
+Use [PostCSS Custom Properties Import/Export] in your Gulpfile:
+
+```js
+const postcss = require('gulp-postcss');
+const postcssCustomPropertiesImportExport = require('@csstools/postcss-custom-properties-import-export');
+
+gulp.task('css', function () {
+ var plugins = [
+ postcssCustomPropertiesImportExport(/* pluginOptions */)
+ ];
+
+ return gulp.src('./src/*.css')
+ .pipe(postcss(plugins))
+ .pipe(gulp.dest('.'));
+});
+```
+
+## Grunt
+
+Add [Grunt PostCSS] to your project:
+
+```bash
+npm install grunt-postcss @csstools/postcss-custom-properties-import-export --save-dev
+```
+
+Use [PostCSS Custom Properties Import/Export] in your Gruntfile:
+
+```js
+const postcssCustomPropertiesImportExport = require('@csstools/postcss-custom-properties-import-export');
+
+grunt.loadNpmTasks('grunt-postcss');
+
+grunt.initConfig({
+ postcss: {
+ options: {
+ processors: [
+ postcssCustomPropertiesImportExport(/* 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 Properties Import/Export]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-properties-import-export
+[React App Rewire PostCSS]: https://github.com/csstools/react-app-rewire-postcss
+[React App Rewired]: https://github.com/timarney/react-app-rewired
+[Next.js]: https://nextjs.org
diff --git a/plugins/postcss-custom-properties-import-export/LICENSE.md b/plugins/postcss-custom-properties-import-export/LICENSE.md
new file mode 100644
index 000000000..6d7047088
--- /dev/null
+++ b/plugins/postcss-custom-properties-import-export/LICENSE.md
@@ -0,0 +1,21 @@
+# 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.
diff --git a/plugins/postcss-custom-properties-import-export/README.md b/plugins/postcss-custom-properties-import-export/README.md
new file mode 100644
index 000000000..bf23b2773
--- /dev/null
+++ b/plugins/postcss-custom-properties-import-export/README.md
@@ -0,0 +1,164 @@
+# PostCSS Custom Properties Import/Export [
][PostCSS]
+
+[
][npm-url] [
][cli-url] [
][discord]
+
+[PostCSS Custom Properties Import/Export] lets you import or export CSS custom properties (`--foo: pink;`) into or out of your CSS.
+
+## As a drop in for old versions of `postcss-custom-properties`
+
+⚠️ `postcss-custom-properties` no longer removes any custom properties.
+If you inject a lot of properties they will all be added to your final CSS.
+Use a separate CSS minifier/optimizer to remove unused custom properties.
+
+```js
+// commonjs
+const postcss = require('postcss');
+const postcssCustomPropertiesImportExport = require('@csstools/postcss-custom-properties-import-export');
+const postcssCustomProperties = require('postcss-custom-properties');
+
+postcss([
+ // First
+ postcssCustomPropertiesImportExport({
+ /* pluginOptions */
+ importedStylesOverrideDocumentStyles: true, // mimics old `postcss-custom-properties`
+ }),
+ // Second
+ postcssCustomProperties()
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+## Usage
+
+Add [PostCSS Custom Properties Import/Export] to your project:
+
+```bash
+npm install postcss @csstools/postcss-custom-properties-import-export --save-dev
+```
+
+Use it as a [PostCSS] plugin:
+
+```js
+const postcss = require('postcss');
+const postcssCustomPropertiesImportExport = require('@csstools/postcss-custom-properties-import-export');
+
+postcss([
+ postcssCustomPropertiesImportExport(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+[PostCSS Custom Properties Import/Export] runs in all Node environments, with special
+instructions for:
+
+- [Node](INSTALL.md#node)
+- [PostCSS CLI](INSTALL.md#postcss-cli)
+- [PostCSS Load Config](INSTALL.md#postcss-load-config)
+- [Webpack](INSTALL.md#webpack)
+- [Create React App](INSTALL.md#create-react-app)
+- [Next.js](INSTALL.md#nextjs)
+- [Gulp](INSTALL.md#gulp)
+- [Grunt](INSTALL.md#grunt)
+
+## Options
+
+### importFrom
+
+The `importFrom` option specifies sources where custom properties can be
+imported from, which might be CSS, JS, and JSON files, functions, and directly
+passed objects.
+
+```js
+postcssCustomPropertiesImportExport({
+ importFrom: 'path/to/file.css' // => :root { --color: var(rgb(245 20 255)); }
+});
+```
+
+```pcss
+article {
+ color: var(--color);
+}
+
+/* becomes */
+
+:root {
+ --color: var(rgb(245 20 255));
+}
+
+article {
+ color: var(--color);
+}
+```
+
+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 properties using the `customProperties` or
+`custom-properties` key.
+
+```js
+postcssCustomPropertiesImportExport({
+ importFrom: [
+ 'path/to/file.css',
+ 'and/then/this.js',
+ 'and/then/that.json',
+ {
+ customProperties: { '--color': 'var(rgb(245 20 255))' }
+ },
+ () => {
+ const customProperties = { '--color': 'var(rgb(245 20 255))' };
+
+ return { customProperties };
+ }
+ ]
+});
+```
+
+### importedStylesOverrideDocumentStyles
+
+The `importedStylesOverrideDocumentStyles` option determines if properties added via `importFrom` override properties that exist in your CSS document.
+Defaults to `false`.
+
+```js
+postcssCustomPropertiesImportExport({
+ importedStylesOverrideDocumentStyles: true
+});
+
+### exportTo
+
+The `exportTo` option specifies destinations where custom properties can be
+exported to, which might be CSS, JS, and JSON files, functions, and directly
+passed objects.
+
+```js
+postcssCustomPropertiesImportExport({
+ exportTo: 'path/to/file.css' // :root { --color: var(rgb(245 20 255)); }
+});
+```
+
+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 properties using the `customProperties` or
+`custom-properties` key.
+
+```js
+const cachedObject = { customProperties: {} };
+
+postcssCustomPropertiesImportExport({
+ exportTo: [
+ 'path/to/file.css', // :root { --color: var(rgb(245 20 255)); }
+ 'and/then/this.js', // module.exports = { customProperties: { '--color': 'var(rgb(245 20 255))' } }
+ 'and/then/this.mjs', // export const customProperties = { '--color': 'var(rgb(245 20 255))' } }
+ 'and/then/that.json', // { "custom-properties": { "--color": "var(rgb(245 20 255))" } }
+ cachedObject,
+ (customProperties) => {
+ customProperties // { '--color': 'var(rgb(245 20 255))' }
+ }
+ ]
+});
+```
+
+[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test
+
+[discord]: https://discord.gg/bUadyRwkJS
+[npm-url]: https://www.npmjs.com/package/@csstools/postcss-custom-properties-import-export
+
+[PostCSS]: https://github.com/postcss/postcss
+[PostCSS Custom Properties Import/Export]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-properties-import-export
diff --git a/plugins/postcss-custom-properties-import-export/docs/README.md b/plugins/postcss-custom-properties-import-export/docs/README.md
new file mode 100644
index 000000000..3df3822e4
--- /dev/null
+++ b/plugins/postcss-custom-properties-import-export/docs/README.md
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[] lets you import or export CSS custom properties (`--foo: pink;`) into or out of your CSS.
+
+## As a drop in for old versions of `postcss-custom-properties`
+
+⚠️ `postcss-custom-properties` no longer removes any custom properties.
+If you inject a lot of properties they will all be added to your final CSS.
+Use a separate CSS minifier/optimizer to remove unused custom properties.
+
+```js
+// commonjs
+const postcss = require('postcss');
+const postcssCustomPropertiesImportExport = require('@csstools/postcss-custom-properties-import-export');
+const postcssCustomProperties = require('postcss-custom-properties');
+
+postcss([
+ // First
+ postcssCustomPropertiesImportExport({
+ /* pluginOptions */
+ importedStylesOverrideDocumentStyles: true, // mimics old `postcss-custom-properties`
+ }),
+ // Second
+ postcssCustomProperties()
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+
+
+
+
+## Options
+
+### importFrom
+
+The `importFrom` option specifies sources where custom properties can be
+imported from, which might be CSS, JS, and JSON files, functions, and directly
+passed objects.
+
+```js
+({
+ importFrom: 'path/to/file.css' // => :root { --color: var(rgb(245 20 255)); }
+});
+```
+
+```pcss
+article {
+ color: var(--color);
+}
+
+/* becomes */
+
+:root {
+ --color: var(rgb(245 20 255));
+}
+
+article {
+ color: var(--color);
+}
+```
+
+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 properties using the `customProperties` or
+`custom-properties` key.
+
+```js
+({
+ importFrom: [
+ 'path/to/file.css',
+ 'and/then/this.js',
+ 'and/then/that.json',
+ {
+ customProperties: { '--color': 'var(rgb(245 20 255))' }
+ },
+ () => {
+ const customProperties = { '--color': 'var(rgb(245 20 255))' };
+
+ return { customProperties };
+ }
+ ]
+});
+```
+
+### importedStylesOverrideDocumentStyles
+
+The `importedStylesOverrideDocumentStyles` option determines if properties added via `importFrom` override properties that exist in your CSS document.
+Defaults to `false`.
+
+```js
+({
+ importedStylesOverrideDocumentStyles: true
+});
+
+### exportTo
+
+The `exportTo` option specifies destinations where custom properties can be
+exported to, which might be CSS, JS, and JSON files, functions, and directly
+passed objects.
+
+```js
+({
+ exportTo: 'path/to/file.css' // :root { --color: var(rgb(245 20 255)); }
+});
+```
+
+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 properties using the `customProperties` or
+`custom-properties` key.
+
+```js
+const cachedObject = { customProperties: {} };
+
+({
+ exportTo: [
+ 'path/to/file.css', // :root { --color: var(rgb(245 20 255)); }
+ 'and/then/this.js', // module.exports = { customProperties: { '--color': 'var(rgb(245 20 255))' } }
+ 'and/then/this.mjs', // export const customProperties = { '--color': 'var(rgb(245 20 255))' } }
+ 'and/then/that.json', // { "custom-properties": { "--color": "var(rgb(245 20 255))" } }
+ cachedObject,
+ (customProperties) => {
+ customProperties // { '--color': 'var(rgb(245 20 255))' }
+ }
+ ]
+});
+```
+
+
diff --git a/plugins/postcss-custom-properties-import-export/package.json b/plugins/postcss-custom-properties-import-export/package.json
new file mode 100644
index 000000000..59f149cab
--- /dev/null
+++ b/plugins/postcss-custom-properties-import-export/package.json
@@ -0,0 +1,91 @@
+{
+ "name": "@csstools/postcss-custom-properties-import-export",
+ "description": "Import/Export Custom Properties in CSS",
+ "version": "1.0.0",
+ "contributors": [
+ {
+ "name": "Antonio Laguna",
+ "email": "antonio@laguna.es",
+ "url": "https://antonio.laguna.es"
+ },
+ {
+ "name": "Romain Menke",
+ "email": "romainmenke@gmail.com"
+ },
+ {
+ "name": "Jonathan Neal",
+ "email": "jonathantneal@hotmail.com"
+ },
+ {
+ "name": "Maxime Thirouin"
+ }
+ ],
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "main": "dist/index.cjs",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.cjs",
+ "default": "./dist/index.mjs"
+ }
+ },
+ "files": [
+ "CHANGELOG.md",
+ "LICENSE.md",
+ "README.md",
+ "dist",
+ "index.d.ts"
+ ],
+ "peerDependencies": {
+ "postcss": "^8.4"
+ },
+ "devDependencies": {
+ "postcss-custom-properties": "^13.0.0",
+ "postcss-import": "^15.0.0"
+ },
+ "scripts": {
+ "build": "rollup -c ../../rollup/default.mjs",
+ "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"",
+ "docs": "node ../../.github/bin/generate-docs/install.mjs && node ../../.github/bin/generate-docs/readme.mjs",
+ "lint": "npm run lint:eslint && npm run lint:package-json",
+ "lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern",
+ "lint:package-json": "node ../../.github/bin/format-package-json.mjs",
+ "prepublishOnly": "npm run clean && npm run build && npm run test",
+ "test": "node .tape.mjs && node .tape.cjs && npm run test:exports",
+ "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs",
+ "test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.mjs"
+ },
+ "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-properties-import-export#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/csstools/postcss-plugins.git",
+ "directory": "plugins/postcss-custom-properties-import-export"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "css",
+ "custom",
+ "declarations",
+ "postcss",
+ "postcss-plugin",
+ "properties",
+ "variables",
+ "vars"
+ ],
+ "csstools": {
+ "exportName": "postcssCustomPropertiesImportExport",
+ "humanReadableName": "PostCSS Custom Properties Import/Export"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/plugins/postcss-custom-properties-import-export/src/index.ts b/plugins/postcss-custom-properties-import-export/src/index.ts
new file mode 100644
index 000000000..96ff55c0c
--- /dev/null
+++ b/plugins/postcss-custom-properties-import-export/src/index.ts
@@ -0,0 +1,119 @@
+import getCustomPropertiesFromImports from './lib/get-custom-properties-from-imports';
+import getCustomPropertiesFromRoot from './lib/get-custom-properties-from-root';
+import writeCustomPropertiesToExports from './lib/write-custom-properties-to-exports';
+import type { ImportOptions, ExportOptions } from './lib/options';
+import type { PluginCreator } from 'postcss';
+
+export interface PluginOptions {
+ /** Specifies sources where Custom Properties can be imported from, which might be CSS, JS, and JSON files, functions, and directly passed objects. */
+ importFrom?: ImportOptions | Array
+
+ /** Specifies destinations where Custom Properties can be exported to, which might be CSS, JS, and JSON files, functions, and directly passed objects. */
+ exportTo?: ExportOptions | Array
+
+ /** Specifies if `importFrom` properties or `:root` properties have priority. */
+ importedStylesOverrideDocumentStyles?: boolean
+}
+
+const creator: PluginCreator = (opts?: PluginOptions) => {
+ const importedStylesOverrideDocumentStyles = 'importedStylesOverrideDocumentStyles' in Object(opts) ? Boolean(opts.importedStylesOverrideDocumentStyles) : false;
+
+ // sources to import custom selectors from
+ let importFrom: Array = [];
+ if (Array.isArray(opts?.importFrom)) {
+ importFrom = opts.importFrom;
+ } else if (opts?.importFrom) {
+ importFrom = [opts.importFrom];
+ }
+
+ // destinations to export custom selectors to
+ let exportTo: Array = [];
+ if (Array.isArray(opts?.exportTo)) {
+ exportTo = opts.exportTo;
+ } else if (opts?.exportTo) {
+ exportTo = [opts.exportTo];
+ }
+
+ // promise any custom selectors are imported
+ const customPropertiesPromise = getCustomPropertiesFromImports(importFrom);
+
+ return {
+ postcssPlugin: 'postcss-custom-properties-import-export',
+ prepare () {
+ const customPropertiesForInsertion: Map = new Map();
+ const customPropertiesForExport: Map = new Map();
+
+ return {
+ Once: async (root, { result, postcss }) => {
+ const importedCustomerProperties = (await customPropertiesPromise).entries();
+ const rootCustomProperties = getCustomPropertiesFromRoot(root).entries();
+
+ for (const [name, value] of importedCustomerProperties) {
+ customPropertiesForInsertion.set(name, value);
+ }
+
+ if (importedStylesOverrideDocumentStyles) {
+ for (const [name, value] of [...rootCustomProperties, ...importedCustomerProperties]) {
+ customPropertiesForExport.set(name, value);
+ }
+ } else {
+ for (const [name, value] of [...importedCustomerProperties, ...rootCustomProperties]) {
+ customPropertiesForExport.set(name, value);
+ }
+ }
+
+ await writeCustomPropertiesToExports(customPropertiesForExport, exportTo);
+
+ if (customPropertiesForInsertion.size > 0) {
+ const propertyNames = Array.from(customPropertiesForInsertion.keys());
+ // Inserting in reverse order results in the correct order.
+ propertyNames.reverse();
+
+ let operator = 'prepend';
+ if (importedStylesOverrideDocumentStyles) {
+ operator = 'append';
+ }
+
+ const rootRule = postcss.rule({
+ selector: ':root',
+ source: {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ input: {
+ from: result.opts.from,
+ },
+ start: { offset: 0, line: 1, column: 1 },
+ end: { offset: 0, line: 1, column: 1 },
+ },
+ });
+
+ root[operator](rootRule);
+
+ propertyNames.forEach((propertyName) => {
+ const decl = postcss.decl({
+ prop: propertyName,
+ value: customPropertiesForInsertion.get(propertyName),
+ });
+
+ decl.source = {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ input: {
+ from: result.opts.from,
+ },
+ start: { offset: 0, line: 1, column: 1 },
+ end: { offset: 0, line: 1, column: 1 },
+ };
+
+ rootRule.append(decl);
+ });
+ }
+ },
+ };
+ },
+ };
+};
+
+creator.postcss = true;
+
+export default creator;
diff --git a/plugins/postcss-custom-properties-import-export/src/lib/get-custom-properties-from-imports.ts b/plugins/postcss-custom-properties-import-export/src/lib/get-custom-properties-from-imports.ts
new file mode 100644
index 000000000..a1f354e03
--- /dev/null
+++ b/plugins/postcss-custom-properties-import-export/src/lib/get-custom-properties-from-imports.ts
@@ -0,0 +1,150 @@
+import getCustomPropertiesFromRoot from './get-custom-properties-from-root';
+import path from 'path';
+import { pathToFileURL } from 'url';
+import type { ImportCustomProperties, ImportOptions } from './options';
+import { parse } from 'postcss';
+import { promises as fsp } from 'fs';
+
+/* Get Custom Properties from CSS File
+/* ========================================================================== */
+
+async function getCustomPropertiesFromCSSFile(from) {
+ const css = await fsp.readFile(pathToFileURL(path.resolve(from)));
+ const root = parse(css, { from : from.toString() });
+
+ return getCustomPropertiesFromRoot(root);
+}
+
+/* Get Custom Properties from Object
+/* ========================================================================== */
+
+function getCustomPropertiesFromObject(object: ImportCustomProperties): Map {
+ const out: Map = new Map();
+
+ if ('customProperties' in object) {
+ for (const [name, value] of Object.entries(object.customProperties)) {
+ out.set(name, value.toString());
+ }
+ }
+
+ if ('custom-properties' in object) {
+ for (const [name, value] of Object.entries(object['custom-properties'])) {
+ out.set(name, value.toString());
+ }
+ }
+
+ return out;
+}
+
+/* Get Custom Properties from JSON file
+/* ========================================================================== */
+
+async function getCustomPropertiesFromJSONFile(from): Promise