diff --git a/.github/ISSUE_TEMPLATE/css-issue.yml b/.github/ISSUE_TEMPLATE/css-issue.yml
index b6f4ba6c9..9fe9251c8 100644
--- a/.github/ISSUE_TEMPLATE/css-issue.yml
+++ b/.github/ISSUE_TEMPLATE/css-issue.yml
@@ -113,6 +113,7 @@ body:
- PostCSS Media MinMax
- PostCSS Media Queries Aspect-Ratio Number Values
- PostCSS Minify
+ - PostCSS Mixins
- PostCSS Nested Calc
- PostCSS Nesting
- PostCSS OKLab Function
diff --git a/.github/ISSUE_TEMPLATE/plugin-issue.yml b/.github/ISSUE_TEMPLATE/plugin-issue.yml
index 70d7be639..e287c1a2f 100644
--- a/.github/ISSUE_TEMPLATE/plugin-issue.yml
+++ b/.github/ISSUE_TEMPLATE/plugin-issue.yml
@@ -110,6 +110,7 @@ body:
- PostCSS Media MinMax
- PostCSS Media Queries Aspect-Ratio Number Values
- PostCSS Minify
+ - PostCSS Mixins
- PostCSS Nested Calc
- PostCSS Nesting
- PostCSS OKLab Function
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 7219806ef..449c844b4 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -353,6 +353,12 @@
- plugins/postcss-minify/**
- experimental/postcss-minify/**
+"plugins/postcss-mixins":
+ - changed-files:
+ - any-glob-to-any-file:
+ - plugins/postcss-mixins/**
+ - experimental/postcss-mixins/**
+
"plugins/postcss-nested-calc":
- changed-files:
- any-glob-to-any-file:
diff --git a/package-lock.json b/package-lock.json
index d30292e0c..e98abdac0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2069,6 +2069,10 @@
"resolved": "plugins/postcss-minify",
"link": true
},
+ "node_modules/@csstools/postcss-mixins": {
+ "resolved": "plugins/postcss-mixins",
+ "link": true
+ },
"node_modules/@csstools/postcss-nested-calc": {
"resolved": "plugins/postcss-nested-calc",
"link": true
@@ -5375,9 +5379,9 @@
"license": "CC0-1.0"
},
"node_modules/cssdb": {
- "version": "8.6.0",
- "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.6.0.tgz",
- "integrity": "sha512-7ZrRi/Z3cRL1d5I8RuXEWAkRFP3J4GeQRiyVknI4KC70RAU8hT4LysUZDe0y+fYNOktCbxE8sOPUOhyR12UqGQ==",
+ "version": "8.7.0",
+ "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.7.0.tgz",
+ "integrity": "sha512-UxiWVpV953ENHqAKjKRPZHNDfRo3uOymvO5Ef7MFCWlenaohkYj7PTO7WCBdjZm8z/aDZd6rXyUIlwZ0AjyFSg==",
"funding": [
{
"type": "opencollective",
@@ -7925,6 +7929,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -8817,6 +8822,98 @@
"stylelint": "^16.0.1"
}
},
+ "node_modules/stylelint/node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/stylelint/node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/stylelint/node_modules/@csstools/media-query-list-parser": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz",
+ "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/stylelint/node_modules/@csstools/selector-specificity": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz",
+ "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ }
+ },
"node_modules/stylelint/node_modules/balanced-match": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz",
@@ -9977,7 +10074,7 @@
"css-blank-pseudo": "^8.0.1",
"css-has-pseudo": "^8.0.0",
"css-prefers-color-scheme": "^11.0.0",
- "cssdb": "^8.6.0",
+ "cssdb": "^8.7.0",
"postcss-attribute-case-insensitive": "^8.0.0",
"postcss-clamp": "^4.1.0",
"postcss-color-functional-notation": "^8.0.0",
@@ -11555,6 +11652,34 @@
"postcss": "^8.4"
}
},
+ "plugins/postcss-mixins": {
+ "name": "@csstools/postcss-mixins",
+ "version": "0.0.0",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "dependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ },
+ "devDependencies": {
+ "@csstools/postcss-tape": "*"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
"plugins/postcss-nested-calc": {
"name": "@csstools/postcss-nested-calc",
"version": "5.0.0",
diff --git a/plugin-packs/postcss-preset-env/package.json b/plugin-packs/postcss-preset-env/package.json
index 04598c391..351d3c888 100644
--- a/plugin-packs/postcss-preset-env/package.json
+++ b/plugin-packs/postcss-preset-env/package.json
@@ -90,7 +90,7 @@
"css-blank-pseudo": "^8.0.1",
"css-has-pseudo": "^8.0.0",
"css-prefers-color-scheme": "^11.0.0",
- "cssdb": "^8.6.0",
+ "cssdb": "^8.7.0",
"postcss-attribute-case-insensitive": "^8.0.0",
"postcss-clamp": "^4.1.0",
"postcss-color-functional-notation": "^8.0.0",
diff --git a/plugins/postcss-mixins/.gitignore b/plugins/postcss-mixins/.gitignore
new file mode 100644
index 000000000..e5b28db4a
--- /dev/null
+++ b/plugins/postcss-mixins/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+package-lock.json
+yarn.lock
+*.result.css
+*.result.css.map
+*.result.html
diff --git a/plugins/postcss-mixins/.nvmrc b/plugins/postcss-mixins/.nvmrc
new file mode 100644
index 000000000..28d6ff1c8
--- /dev/null
+++ b/plugins/postcss-mixins/.nvmrc
@@ -0,0 +1 @@
+v25.1.0
diff --git a/plugins/postcss-mixins/CHANGELOG.md b/plugins/postcss-mixins/CHANGELOG.md
new file mode 100644
index 000000000..1aab830c3
--- /dev/null
+++ b/plugins/postcss-mixins/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changes to PostCSS Mixins
+
+### Unreleased (major)
+
+- Initial version
diff --git a/plugins/postcss-mixins/INSTALL.md b/plugins/postcss-mixins/INSTALL.md
new file mode 100644
index 000000000..84a2922e1
--- /dev/null
+++ b/plugins/postcss-mixins/INSTALL.md
@@ -0,0 +1,235 @@
+# Installing PostCSS Mixins
+
+[PostCSS Mixins] runs in all Node environments, with special instructions for:
+
+- [Node](#node)
+- [PostCSS CLI](#postcss-cli)
+- [PostCSS Load Config](#postcss-load-config)
+- [Webpack](#webpack)
+- [Next.js](#nextjs)
+- [Gulp](#gulp)
+- [Grunt](#grunt)
+
+
+
+## Node
+
+Add [PostCSS Mixins] to your project:
+
+```bash
+npm install postcss @csstools/postcss-mixins --save-dev
+```
+
+Use it as a [PostCSS] plugin:
+
+```js
+// commonjs
+const postcss = require('postcss');
+const postcssMixins = require('@csstools/postcss-mixins');
+
+postcss([
+ postcssMixins(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+```js
+// esm
+import postcss from 'postcss';
+import postcssMixins from '@csstools/postcss-mixins';
+
+postcss([
+ postcssMixins(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+## PostCSS CLI
+
+Add [PostCSS CLI] to your project:
+
+```bash
+npm install postcss-cli @csstools/postcss-mixins --save-dev
+```
+
+Use [PostCSS Mixins] in your `postcss.config.js` configuration file:
+
+```js
+const postcssMixins = require('@csstools/postcss-mixins');
+
+module.exports = {
+ plugins: [
+ postcssMixins(/* pluginOptions */)
+ ]
+}
+```
+
+## PostCSS Load Config
+
+If your framework/CLI supports [`postcss-load-config`](https://github.com/postcss/postcss-load-config).
+
+```bash
+npm install @csstools/postcss-mixins --save-dev
+```
+
+`package.json`:
+
+```json
+{
+ "postcss": {
+ "plugins": {
+ "@csstools/postcss-mixins": {}
+ }
+ }
+}
+```
+
+`.postcssrc.json`:
+
+```json
+{
+ "plugins": {
+ "@csstools/postcss-mixins": {}
+ }
+}
+```
+
+_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-mixins --save-dev
+```
+
+Use [PostCSS Mixins] 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: [
+ // Other plugins,
+ [
+ "@csstools/postcss-mixins",
+ {
+ // Options
+ },
+ ],
+ ],
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+};
+```
+
+## 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-mixins --save-dev
+```
+
+Use [PostCSS Mixins] in your `postcss.config.json` file:
+
+```json
+{
+ "plugins": [
+ "@csstools/postcss-mixins"
+ ]
+}
+```
+
+```json5
+{
+ "plugins": [
+ [
+ "@csstools/postcss-mixins",
+ {
+ // Optionally add plugin options
+ }
+ ]
+ ]
+}
+```
+
+## Gulp
+
+Add [Gulp PostCSS] to your project:
+
+```bash
+npm install gulp-postcss @csstools/postcss-mixins --save-dev
+```
+
+Use [PostCSS Mixins] in your Gulpfile:
+
+```js
+const postcss = require('gulp-postcss');
+const postcssMixins = require('@csstools/postcss-mixins');
+
+gulp.task('css', function () {
+ var plugins = [
+ postcssMixins(/* 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-mixins --save-dev
+```
+
+Use [PostCSS Mixins] in your Gruntfile:
+
+```js
+const postcssMixins = require('@csstools/postcss-mixins');
+
+grunt.loadNpmTasks('grunt-postcss');
+
+grunt.initConfig({
+ postcss: {
+ options: {
+ processors: [
+ postcssMixins(/* 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 Mixins]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-mixins
+[Next.js]: https://nextjs.org
diff --git a/plugins/postcss-mixins/LICENSE.md b/plugins/postcss-mixins/LICENSE.md
new file mode 100644
index 000000000..e8ae93b9f
--- /dev/null
+++ b/plugins/postcss-mixins/LICENSE.md
@@ -0,0 +1,18 @@
+MIT No Attribution (MIT-0)
+
+Copyright © CSSTools Contributors
+
+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.
+
+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-mixins/README.md b/plugins/postcss-mixins/README.md
new file mode 100644
index 000000000..c4b81f603
--- /dev/null
+++ b/plugins/postcss-mixins/README.md
@@ -0,0 +1,106 @@
+# PostCSS Mixins [
][PostCSS]
+
+[
][npm-url] [
][cli-url] [
][discord]
[
][css-url] [
][css-url]
+
+```bash
+npm install @csstools/postcss-mixins --save-dev
+```
+
+[PostCSS Mixins] lets you use `@mixin` and `@apply` following [CSS Mixins 1].
+
+Several specification aspects of CSS Mixins still need to be settled.
+This plugin is only a partial implementation to avoid conflicts with the final specification.
+
+Unsupported:
+- mixin arguments
+- `@contents` blocks
+- `@result` blocks
+- layered `@mixin` declarations
+- mixin overrides
+
+```css
+@mixin --foo() {
+ color: green;
+}
+
+.foo {
+ @apply --foo;
+}
+
+/* becomes */
+
+.foo {
+ color: green;
+}
+```
+
+## Usage
+
+Add [PostCSS Mixins] to your project:
+
+```bash
+npm install postcss @csstools/postcss-mixins --save-dev
+```
+
+Use it as a [PostCSS] plugin:
+
+```js
+const postcss = require('postcss');
+const postcssMixins = require('@csstools/postcss-mixins');
+
+postcss([
+ postcssMixins(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+[PostCSS Mixins] 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)
+- [Next.js](INSTALL.md#nextjs)
+- [Gulp](INSTALL.md#gulp)
+- [Grunt](INSTALL.md#grunt)
+
+## Options
+
+### preserve
+
+The `preserve` option determines whether the original notation
+is preserved. By default, it is not preserved.
+
+```js
+postcssMixins({ preserve: true })
+```
+
+```css
+@mixin --foo() {
+ color: green;
+}
+
+.foo {
+ @apply --foo;
+}
+
+/* becomes */
+
+@mixin --foo() {
+ color: green;
+}
+
+.foo {
+ color: green;
+ @apply --foo;
+}
+```
+
+[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test
+[css-url]: https://cssdb.org/#mixins
+[discord]: https://discord.gg/bUadyRwkJS
+[npm-url]: https://www.npmjs.com/package/@csstools/postcss-mixins
+
+[PostCSS]: https://github.com/postcss/postcss
+[PostCSS Mixins]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-mixins
+[CSS Mixins 1]: https://drafts.csswg.org/css-mixins/#mixin-rule
diff --git a/plugins/postcss-mixins/api-extractor.json b/plugins/postcss-mixins/api-extractor.json
new file mode 100644
index 000000000..42058be51
--- /dev/null
+++ b/plugins/postcss-mixins/api-extractor.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
+ "extends": "../../api-extractor.json"
+}
diff --git a/plugins/postcss-mixins/dist/index.d.ts b/plugins/postcss-mixins/dist/index.d.ts
new file mode 100644
index 000000000..9d1b1b2f3
--- /dev/null
+++ b/plugins/postcss-mixins/dist/index.d.ts
@@ -0,0 +1,13 @@
+import type { PluginCreator } from 'postcss';
+
+declare const creator: PluginCreator;
+export default creator;
+export { creator as 'module.exports' }
+
+/** postcss-mixins plugin options */
+export declare type pluginOptions = {
+ /** Preserve the original notation. default: false */
+ preserve?: boolean;
+};
+
+export { }
diff --git a/plugins/postcss-mixins/dist/index.mjs b/plugins/postcss-mixins/dist/index.mjs
new file mode 100644
index 000000000..37805817a
--- /dev/null
+++ b/plugins/postcss-mixins/dist/index.mjs
@@ -0,0 +1 @@
+import{parseComponentValue as e,isTokenNode as s,isFunctionNode as t}from"@csstools/css-parser-algorithms";import{tokenize as r,isTokenIdent as n}from"@csstools/css-tokenizer";const o=/^apply$/i;function processableApplyRule(o){if(!o.params||!o.params.includes("--"))return!1;if(!isInStyleRule(o))return!1;if(o.nodes?.length)return!1;const a=e(r({css:o.params}));return s(a)&&n(a.value)?a.value[4].value:!!t(a)&&(!a.value.length&&a.getName())}const a=/^scope$/i;function isInStyleRule(e){const s=e.parent;return!(!s||"root"===s.type)&&("rule"===s.type||(!("atrule"!==s.type||!a.test(s.name))||isInStyleRule(s)))}const l=/^(?:apply|contents|result)$/i;function processableMixinRule(s){if("mixin"!==s.name.toLowerCase())return!1;if(!s.params||!s.params.includes("--"))return!1;if(!s.nodes?.length)return!1;if(s.parent!==s.root())return!1;const n=e(r({css:s.params}));if(!t(n))return!1;if(n.value.length)return!1;let o=!1;return s.walk(e=>{"atrule"===e.type&&l.test(e.name)&&(o=!0)}),!o&&n.getName()}const creator=e=>{const s=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-mixins",prepare(){const e=new Map,t=new Set;return{postcssPlugin:"mixins",Once(r){r.each(s=>{if("atrule"!==s.type)return;const r=processableMixinRule(s);r&&(t.has(r)?e.delete(r):(e.set(r,s),t.add(r)))});for(const t of e.values())s.preserve||t.remove();r.walkAtRules(o,t=>{const r=processableApplyRule(t);if(!r)return;const n=e.get(r);n&&n.nodes&&(n.each(e=>{t.before(e.clone())}),s.preserve||t.remove())})}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"};
diff --git a/plugins/postcss-mixins/docs/README.md b/plugins/postcss-mixins/docs/README.md
new file mode 100644
index 000000000..a02389150
--- /dev/null
+++ b/plugins/postcss-mixins/docs/README.md
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[] lets you use `@mixin` and `@apply` following [CSS Mixins 1].
+
+Several specification aspects of CSS Mixins still need to be settled.
+This plugin is only a partial implementation to avoid conflicts with the final specification.
+
+Unsupported:
+- mixin arguments
+- `@contents` blocks
+- `@result` blocks
+- layered `@mixin` declarations
+- mixin overrides
+
+```css
+
+
+/* becomes */
+
+
+```
+
+
+
+
+
+## Options
+
+### preserve
+
+The `preserve` option determines whether the original notation
+is preserved. By default, it is not preserved.
+
+```js
+({ preserve: true })
+```
+
+```css
+
+
+/* becomes */
+
+
+```
+
+
+[CSS Mixins 1]:
diff --git a/plugins/postcss-mixins/package.json b/plugins/postcss-mixins/package.json
new file mode 100644
index 000000000..d69ef4cf8
--- /dev/null
+++ b/plugins/postcss-mixins/package.json
@@ -0,0 +1,80 @@
+{
+ "name": "@csstools/postcss-mixins",
+ "description": "Use mixins in CSS",
+ "version": "0.0.0",
+ "contributors": [
+ {
+ "name": "Antonio Laguna",
+ "email": "antonio@laguna.es",
+ "url": "https://antonio.laguna.es"
+ },
+ {
+ "name": "Romain Menke",
+ "email": "romainmenke@gmail.com"
+ }
+ ],
+ "license": "MIT-0",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.mjs"
+ }
+ },
+ "files": [
+ "CHANGELOG.md",
+ "LICENSE.md",
+ "README.md",
+ "dist"
+ ],
+ "dependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ },
+ "devDependencies": {
+ "@csstools/postcss-tape": "*"
+ },
+ "scripts": {
+ "build": "rollup -c ../../rollup/default.mjs",
+ "docs": "node ../../.github/bin/generate-docs/install.mjs && node ../../.github/bin/generate-docs/readme.mjs",
+ "lint": "node ../../.github/bin/format-package-json.mjs",
+ "prepublishOnly": "npm run build && npm run test",
+ "test": "node --test",
+ "test:rewrite-expects": "REWRITE_EXPECTS=true node --test"
+ },
+ "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-mixins#readme",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/csstools/postcss-plugins.git",
+ "directory": "plugins/postcss-mixins"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "postcss-plugin"
+ ],
+ "csstools": {
+ "cssdbId": "mixins",
+ "exportName": "postcssMixins",
+ "humanReadableName": "PostCSS Mixins",
+ "specUrl": "https://drafts.csswg.org/css-mixins/#mixin-rule"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/plugins/postcss-mixins/src/index.ts b/plugins/postcss-mixins/src/index.ts
new file mode 100644
index 000000000..f3623a6bf
--- /dev/null
+++ b/plugins/postcss-mixins/src/index.ts
@@ -0,0 +1,80 @@
+import type { AtRule, Plugin, PluginCreator } from 'postcss';
+import { IS_APPLY_REGEX, processableApplyRule } from './processable-apply';
+import { processableMixinRule } from './processable-mixin';
+
+/** postcss-mixins plugin options */
+export type pluginOptions = {
+ /** Preserve the original notation. default: false */
+ preserve?: boolean,
+};
+
+const creator: PluginCreator = (opts?: pluginOptions) => {
+ const options: pluginOptions = Object.assign(
+ // Default options
+ {
+ preserve: false,
+ },
+ // Provided options
+ opts,
+ );
+
+ return {
+ postcssPlugin: 'postcss-mixins',
+ prepare(): Plugin {
+ const mixins: Map = new Map();
+ const knownMixins: Set = new Set();
+
+ return {
+ postcssPlugin: 'mixins',
+ Once(root): void {
+ root.each((child) => {
+ if (child.type !== 'atrule') {
+ return;
+ }
+
+ const mixinName = processableMixinRule(child);
+ if (!mixinName) {
+ return;
+ }
+
+ // TODO: support mixin overrides
+ if (knownMixins.has(mixinName)) {
+ mixins.delete(mixinName);
+ return;
+ }
+
+ mixins.set(mixinName, child);
+ knownMixins.add(mixinName);
+ });
+
+ for (const child of mixins.values()) {
+ if (!options.preserve) child.remove();
+ }
+
+ root.walkAtRules(IS_APPLY_REGEX, (atRule) => {
+ const mixinName = processableApplyRule(atRule);
+ if (!mixinName) {
+ return;
+ }
+
+ const mixin = mixins.get(mixinName);
+ if (!mixin || !mixin.nodes) {
+ return;
+ }
+
+ mixin.each((mixinNode) => {
+ atRule.before(mixinNode.clone());
+ });
+
+ if (!options.preserve) atRule.remove();
+ });
+ },
+ };
+ },
+ };
+};
+
+creator.postcss = true;
+
+export default creator;
+export { creator as 'module.exports' };
diff --git a/plugins/postcss-mixins/src/processable-apply.ts b/plugins/postcss-mixins/src/processable-apply.ts
new file mode 100644
index 000000000..b6f8a269e
--- /dev/null
+++ b/plugins/postcss-mixins/src/processable-apply.ts
@@ -0,0 +1,57 @@
+import { isFunctionNode, isTokenNode, parseComponentValue } from "@csstools/css-parser-algorithms";
+import { isTokenIdent, tokenize } from "@csstools/css-tokenizer";
+import type { AtRule } from "postcss";
+
+export const IS_APPLY_REGEX = /^apply$/i;
+
+export function processableApplyRule(atRule: AtRule): false|string {
+ if (!atRule.params || !atRule.params.includes('--')) {
+ return false;
+ }
+
+ if (!isInStyleRule(atRule)) {
+ return false;
+ }
+
+ // TODO: support @contents
+ if (atRule.nodes?.length) {
+ return false;
+ }
+
+ const nameNode = parseComponentValue(tokenize({
+ css: atRule.params,
+ }));
+ if (isTokenNode(nameNode) && isTokenIdent(nameNode.value)) {
+ return nameNode.value[4].value;
+ }
+
+ if (!isFunctionNode(nameNode)) {
+ return false;
+ }
+
+ // TODO: support arguments
+ if (nameNode.value.length) {
+ return false;
+ }
+
+ return nameNode.getName();
+}
+
+const IS_SCOPE_REGEX = /^scope$/i;
+
+function isInStyleRule(atRule: AtRule): boolean {
+ const parent = atRule.parent;
+ if (!parent || parent.type === 'root') {
+ return false;
+ }
+
+ if (parent.type === 'rule') {
+ return true;
+ }
+
+ if (parent.type === 'atrule' && IS_SCOPE_REGEX.test(parent.name)) {
+ return true;
+ }
+
+ return isInStyleRule(parent);
+}
diff --git a/plugins/postcss-mixins/src/processable-mixin.ts b/plugins/postcss-mixins/src/processable-mixin.ts
new file mode 100644
index 000000000..b70572f4e
--- /dev/null
+++ b/plugins/postcss-mixins/src/processable-mixin.ts
@@ -0,0 +1,50 @@
+import { isFunctionNode, parseComponentValue } from "@csstools/css-parser-algorithms";
+import { tokenize } from "@csstools/css-tokenizer";
+import type { AtRule } from "postcss";
+
+const IS_IGNORED_CHILD_RULE = /^(?:apply|contents|result)$/i;
+
+export function processableMixinRule(atRule: AtRule): false | string {
+ if (atRule.name.toLowerCase() !== 'mixin') {
+ return false;
+ }
+
+ if (!atRule.params || !atRule.params.includes('--')) {
+ return false;
+ }
+
+ if (!atRule.nodes?.length) {
+ return false;
+ }
+
+ // TODO: support conditional @mixin declarations
+ if (atRule.parent !== atRule.root()) {
+ return false;
+ }
+
+ const nameNode = parseComponentValue(tokenize({
+ css: atRule.params,
+ }));
+ if (!isFunctionNode(nameNode)) {
+ return false;
+ }
+
+ if (nameNode.value.length) {
+ return false;
+ }
+
+ // TODO: support @content
+ // TODO: support nested @apply
+ let hasNestedApplyOrContents = false;
+ atRule.walk((x) => {
+ if (x.type === 'atrule' && IS_IGNORED_CHILD_RULE.test(x.name)) {
+ hasNestedApplyOrContents = true;
+ }
+ });
+
+ if (hasNestedApplyOrContents) {
+ return false;
+ }
+
+ return nameNode.getName();
+}
diff --git a/plugins/postcss-mixins/test/_import.mjs b/plugins/postcss-mixins/test/_import.mjs
new file mode 100644
index 000000000..b1651ee6c
--- /dev/null
+++ b/plugins/postcss-mixins/test/_import.mjs
@@ -0,0 +1,10 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import plugin from '@csstools/postcss-mixins';
+
+test('import', () => {
+ plugin();
+ assert.ok(plugin.postcss, 'should have "postcss flag"');
+ assert.equal(typeof plugin, 'function', 'should return a function');
+});
+
diff --git a/plugins/postcss-mixins/test/_require.cjs b/plugins/postcss-mixins/test/_require.cjs
new file mode 100644
index 000000000..6ef9bff19
--- /dev/null
+++ b/plugins/postcss-mixins/test/_require.cjs
@@ -0,0 +1,9 @@
+const assert = require('node:assert/strict');
+const test = require('node:test');
+const plugin = require('@csstools/postcss-mixins');
+
+test('require', () => {
+ plugin();
+ assert.ok(plugin.postcss, 'should have "postcss flag"');
+ assert.equal(typeof plugin, 'function', 'should return a function');
+});
diff --git a/plugins/postcss-mixins/test/_tape.mjs b/plugins/postcss-mixins/test/_tape.mjs
new file mode 100644
index 000000000..832ea5c67
--- /dev/null
+++ b/plugins/postcss-mixins/test/_tape.mjs
@@ -0,0 +1,36 @@
+import { postcssTape } from '@csstools/postcss-tape';
+import plugin from '@csstools/postcss-mixins';
+
+postcssTape(plugin)({
+ basic: {
+ message: 'supports basic usage',
+ },
+ 'basic:preserve-true': {
+ message: 'supports basic usage with { preserve: true }',
+ options: {
+ preserve: true,
+ },
+ },
+ ignore: {
+ message: 'ignores invalid or unsupported behavior',
+ expect: 'ignore.css',
+ result: 'ignore.css',
+ },
+ 'ignore:preserve-true': {
+ message: 'ignores invalid or unsupported behavior with { preserve: true }',
+ expect: 'ignore.css',
+ result: 'ignore.css',
+ options: {
+ preserve: true,
+ },
+ },
+ 'examples/example': {
+ message: 'minimal example',
+ },
+ 'examples/example:preserve-true': {
+ message: 'minimal example',
+ options: {
+ preserve: true,
+ },
+ },
+});
diff --git a/plugins/postcss-mixins/test/basic.css b/plugins/postcss-mixins/test/basic.css
new file mode 100644
index 000000000..3217680dc
--- /dev/null
+++ b/plugins/postcss-mixins/test/basic.css
@@ -0,0 +1,31 @@
+@mixin --foo() {
+ color: green;
+}
+
+.foo {
+ @apply --foo;
+}
+
+@scope (.foo) {
+ @apply --foo();
+}
+
+@mixin --bar() {
+ color: green;
+
+ @media screen {
+ color: blue;
+ }
+}
+
+.bar {
+ @apply --bar;
+}
+
+.in-between-other-rules {
+ order: 0;
+
+ @apply --bar;
+
+ order: 1;
+}
diff --git a/plugins/postcss-mixins/test/basic.expect.css b/plugins/postcss-mixins/test/basic.expect.css
new file mode 100644
index 000000000..73878cce6
--- /dev/null
+++ b/plugins/postcss-mixins/test/basic.expect.css
@@ -0,0 +1,26 @@
+.foo {
+ color: green;
+}
+
+@scope (.foo) {
+ color: green;
+}
+
+.bar {
+ color: green;
+
+ @media screen {
+ color: blue;
+ }
+}
+
+.in-between-other-rules {
+ order: 0;
+ color: green;
+
+ @media screen {
+ color: blue;
+ }
+
+ order: 1;
+}
diff --git a/plugins/postcss-mixins/test/basic.preserve-true.expect.css b/plugins/postcss-mixins/test/basic.preserve-true.expect.css
new file mode 100644
index 000000000..3900c04f8
--- /dev/null
+++ b/plugins/postcss-mixins/test/basic.preserve-true.expect.css
@@ -0,0 +1,43 @@
+@mixin --foo() {
+ color: green;
+}
+
+.foo {
+ color: green;
+ @apply --foo;
+}
+
+@scope (.foo) {
+ color: green;
+ @apply --foo();
+}
+
+@mixin --bar() {
+ color: green;
+
+ @media screen {
+ color: blue;
+ }
+}
+
+.bar {
+ color: green;
+
+ @media screen {
+ color: blue;
+ }
+ @apply --bar;
+}
+
+.in-between-other-rules {
+ order: 0;
+ color: green;
+
+ @media screen {
+ color: blue;
+ }
+
+ @apply --bar;
+
+ order: 1;
+}
diff --git a/plugins/postcss-mixins/test/examples/example.css b/plugins/postcss-mixins/test/examples/example.css
new file mode 100644
index 000000000..619c5b1ee
--- /dev/null
+++ b/plugins/postcss-mixins/test/examples/example.css
@@ -0,0 +1,7 @@
+@mixin --foo() {
+ color: green;
+}
+
+.foo {
+ @apply --foo;
+}
diff --git a/plugins/postcss-mixins/test/examples/example.expect.css b/plugins/postcss-mixins/test/examples/example.expect.css
new file mode 100644
index 000000000..79d060850
--- /dev/null
+++ b/plugins/postcss-mixins/test/examples/example.expect.css
@@ -0,0 +1,3 @@
+.foo {
+ color: green;
+}
diff --git a/plugins/postcss-mixins/test/examples/example.preserve-true.expect.css b/plugins/postcss-mixins/test/examples/example.preserve-true.expect.css
new file mode 100644
index 000000000..230ea16b6
--- /dev/null
+++ b/plugins/postcss-mixins/test/examples/example.preserve-true.expect.css
@@ -0,0 +1,8 @@
+@mixin --foo() {
+ color: green;
+}
+
+.foo {
+ color: green;
+ @apply --foo;
+}
diff --git a/plugins/postcss-mixins/test/ignore.css b/plugins/postcss-mixins/test/ignore.css
new file mode 100644
index 000000000..9791ec18c
--- /dev/null
+++ b/plugins/postcss-mixins/test/ignore.css
@@ -0,0 +1,79 @@
+@mixin --ignore-a() {
+ color: green;
+}
+
+@mixin --ignore-a() {
+ color: green;
+}
+
+.ignore-a {
+ @apply --ignore-a;
+}
+
+@mixin --ignore-b {
+ color: green;
+}
+
+.ignore-b {
+ @apply --ignore-b;
+}
+
+@mixin --ignore-c {
+ @apply --foo;
+}
+
+.ignore-c {
+ @apply --ignore-c;
+}
+
+@mixin --ignore-d {
+ @contents;
+}
+
+.ignore-d {
+ @apply --ignore-d;
+}
+
+@mixin --ignore-e {
+ color: red;
+}
+
+.ignore-e {
+ @apply --ignore-e {
+ color: red;
+ }
+}
+
+@media screen {
+ @mixin --ignore-f {
+ color: red;
+ }
+}
+
+.ignore-f {
+ @apply --ignore-f;
+}
+
+@mixin --ignore-g {
+ color: red;
+}
+
+@apply --ignore-g;
+
+@mixin --ignore-h {
+ color: red;
+}
+
+@media screen {
+ @apply --ignore-h;
+}
+
+@mixin --ignore-i() {
+ @result {
+ color: green;
+ }
+}
+
+.ignore-i {
+ @apply --ignore-i;
+}
diff --git a/plugins/postcss-mixins/tsconfig.json b/plugins/postcss-mixins/tsconfig.json
new file mode 100644
index 000000000..500af6d26
--- /dev/null
+++ b/plugins/postcss-mixins/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declarationDir": ".",
+ "strict": true
+ },
+ "include": ["./src/**/*"],
+ "exclude": ["dist"]
+}
diff --git a/sites/package-lock.json b/sites/package-lock.json
index 71c260822..614402cf5 100644
--- a/sites/package-lock.json
+++ b/sites/package-lock.json
@@ -5573,9 +5573,9 @@
}
},
"node_modules/cssdb": {
- "version": "8.6.0",
- "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.6.0.tgz",
- "integrity": "sha512-7ZrRi/Z3cRL1d5I8RuXEWAkRFP3J4GeQRiyVknI4KC70RAU8hT4LysUZDe0y+fYNOktCbxE8sOPUOhyR12UqGQ==",
+ "version": "8.7.0",
+ "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.7.0.tgz",
+ "integrity": "sha512-UxiWVpV953ENHqAKjKRPZHNDfRo3uOymvO5Ef7MFCWlenaohkYj7PTO7WCBdjZm8z/aDZd6rXyUIlwZ0AjyFSg==",
"dev": true,
"funding": [
{
@@ -12583,7 +12583,7 @@
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-terser": "^0.4.4",
"codemirror": "^6.0.2",
- "cssdb": "^8.4.2",
+ "cssdb": "^8.7.0",
"luxon": "^3.7.2",
"markdown-it": "^14.1.0",
"npm-run-all": "^4.1.5",
diff --git a/sites/postcss-preset-env/package.json b/sites/postcss-preset-env/package.json
index a57371228..34adf68db 100644
--- a/sites/postcss-preset-env/package.json
+++ b/sites/postcss-preset-env/package.json
@@ -73,7 +73,7 @@
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-terser": "^0.4.4",
"codemirror": "^6.0.2",
- "cssdb": "^8.4.2",
+ "cssdb": "^8.7.0",
"luxon": "^3.7.2",
"markdown-it": "^14.1.0",
"npm-run-all": "^4.1.5",