From e557344fd4501343f40d7aae7b5f113c3f18e16f Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Sun, 30 Aug 2026 18:09:21 +0200 Subject: [PATCH 1/5] postcss-private-rule --- .github/ISSUE_TEMPLATE/css-issue.yml | 1 + .github/ISSUE_TEMPLATE/plugin-issue.yml | 1 + .github/labeler.yml | 6 + package-lock.json | 32 +++ plugins/postcss-private-rule/.gitignore | 6 + plugins/postcss-private-rule/.nvmrc | 1 + plugins/postcss-private-rule/CHANGELOG.md | 5 + plugins/postcss-private-rule/INSTALL.md | 235 ++++++++++++++++++ plugins/postcss-private-rule/LICENSE.md | 18 ++ plugins/postcss-private-rule/README.md | 78 ++++++ .../postcss-private-rule/api-extractor.json | 4 + plugins/postcss-private-rule/dist/index.d.ts | 20 ++ plugins/postcss-private-rule/dist/index.mjs | 1 + plugins/postcss-private-rule/docs/README.md | 35 +++ plugins/postcss-private-rule/package.json | 83 +++++++ plugins/postcss-private-rule/src/index.ts | 175 +++++++++++++ .../postcss-private-rule/src/valid-atrules.ts | 44 ++++ plugins/postcss-private-rule/test/_import.mjs | 10 + .../postcss-private-rule/test/_require.cjs | 9 + plugins/postcss-private-rule/test/_tape.mjs | 11 + plugins/postcss-private-rule/test/basic.css | 145 +++++++++++ .../test/basic.expect.css | 115 +++++++++ .../test/examples/example.css | 13 + .../test/examples/example.expect.css | 11 + plugins/postcss-private-rule/tsconfig.json | 11 + rollup/configs/externals.mjs | 2 + 26 files changed, 1072 insertions(+) create mode 100644 plugins/postcss-private-rule/.gitignore create mode 100644 plugins/postcss-private-rule/.nvmrc create mode 100644 plugins/postcss-private-rule/CHANGELOG.md create mode 100644 plugins/postcss-private-rule/INSTALL.md create mode 100644 plugins/postcss-private-rule/LICENSE.md create mode 100644 plugins/postcss-private-rule/README.md create mode 100644 plugins/postcss-private-rule/api-extractor.json create mode 100644 plugins/postcss-private-rule/dist/index.d.ts create mode 100644 plugins/postcss-private-rule/dist/index.mjs create mode 100644 plugins/postcss-private-rule/docs/README.md create mode 100644 plugins/postcss-private-rule/package.json create mode 100644 plugins/postcss-private-rule/src/index.ts create mode 100644 plugins/postcss-private-rule/src/valid-atrules.ts create mode 100644 plugins/postcss-private-rule/test/_import.mjs create mode 100644 plugins/postcss-private-rule/test/_require.cjs create mode 100644 plugins/postcss-private-rule/test/_tape.mjs create mode 100644 plugins/postcss-private-rule/test/basic.css create mode 100644 plugins/postcss-private-rule/test/basic.expect.css create mode 100644 plugins/postcss-private-rule/test/examples/example.css create mode 100644 plugins/postcss-private-rule/test/examples/example.expect.css create mode 100644 plugins/postcss-private-rule/tsconfig.json diff --git a/.github/ISSUE_TEMPLATE/css-issue.yml b/.github/ISSUE_TEMPLATE/css-issue.yml index be605d0b64..9fb785fc99 100644 --- a/.github/ISSUE_TEMPLATE/css-issue.yml +++ b/.github/ISSUE_TEMPLATE/css-issue.yml @@ -125,6 +125,7 @@ body: - PostCSS Page Break - PostCSS Place - PostCSS Position Area Property + - PostCSS Private Rule - PostCSS Progressive Custom Properties - PostCSS Property Rule Optional Descriptors - PostCSS Property Rule Prelude List diff --git a/.github/ISSUE_TEMPLATE/plugin-issue.yml b/.github/ISSUE_TEMPLATE/plugin-issue.yml index 445d23001b..78c46d0d44 100644 --- a/.github/ISSUE_TEMPLATE/plugin-issue.yml +++ b/.github/ISSUE_TEMPLATE/plugin-issue.yml @@ -122,6 +122,7 @@ body: - PostCSS Page Break - PostCSS Place - PostCSS Position Area Property + - PostCSS Private Rule - PostCSS Progressive Custom Properties - PostCSS Property Rule Optional Descriptors - PostCSS Property Rule Prelude List diff --git a/.github/labeler.yml b/.github/labeler.yml index ac8a44cbed..7aa485ef57 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -431,6 +431,12 @@ - plugins/postcss-position-area-property/** - experimental/postcss-position-area-property/** +"plugins/postcss-private-rule": + - changed-files: + - any-glob-to-any-file: + - plugins/postcss-private-rule/** + - experimental/postcss-private-rule/** + "plugins/postcss-progressive-custom-properties": - changed-files: - any-glob-to-any-file: diff --git a/package-lock.json b/package-lock.json index 214472b11e..3e7bfaf9e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1079,6 +1079,10 @@ "resolved": "plugins/postcss-position-area-property", "link": true }, + "node_modules/@csstools/postcss-private-rule": { + "resolved": "plugins/postcss-private-rule", + "link": true + }, "node_modules/@csstools/postcss-progressive-custom-properties": { "resolved": "plugins/postcss-progressive-custom-properties", "link": true @@ -10519,6 +10523,34 @@ "postcss": "^8.4" } }, + "plugins/postcss-private-rule": { + "name": "@csstools/postcss-private-rule", + "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-progressive-custom-properties": { "name": "@csstools/postcss-progressive-custom-properties", "version": "5.1.2", diff --git a/plugins/postcss-private-rule/.gitignore b/plugins/postcss-private-rule/.gitignore new file mode 100644 index 0000000000..e5b28db4ad --- /dev/null +++ b/plugins/postcss-private-rule/.gitignore @@ -0,0 +1,6 @@ +node_modules +package-lock.json +yarn.lock +*.result.css +*.result.css.map +*.result.html diff --git a/plugins/postcss-private-rule/.nvmrc b/plugins/postcss-private-rule/.nvmrc new file mode 100644 index 0000000000..b807a943cb --- /dev/null +++ b/plugins/postcss-private-rule/.nvmrc @@ -0,0 +1 @@ +v26.1.0 diff --git a/plugins/postcss-private-rule/CHANGELOG.md b/plugins/postcss-private-rule/CHANGELOG.md new file mode 100644 index 0000000000..dacf0f0c18 --- /dev/null +++ b/plugins/postcss-private-rule/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changes to PostCSS Private Rule + +### Unreleased (major) + +- Initial version diff --git a/plugins/postcss-private-rule/INSTALL.md b/plugins/postcss-private-rule/INSTALL.md new file mode 100644 index 0000000000..6ad3cc2bc1 --- /dev/null +++ b/plugins/postcss-private-rule/INSTALL.md @@ -0,0 +1,235 @@ +# Installing PostCSS Private Rule + +[PostCSS Private Rule] 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 Private Rule] to your project: + +```bash +npm install postcss @csstools/postcss-private-rule --save-dev +``` + +Use it as a [PostCSS] plugin: + +```js +// commonjs +const postcss = require('postcss'); +const postcssPrivateRule = require('@csstools/postcss-private-rule'); + +postcss([ + postcssPrivateRule(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +```js +// esm +import postcss from 'postcss'; +import postcssPrivateRule from '@csstools/postcss-private-rule'; + +postcss([ + postcssPrivateRule(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +## PostCSS CLI + +Add [PostCSS CLI] to your project: + +```bash +npm install postcss-cli @csstools/postcss-private-rule --save-dev +``` + +Use [PostCSS Private Rule] in your `postcss.config.js` configuration file: + +```js +const postcssPrivateRule = require('@csstools/postcss-private-rule'); + +module.exports = { + plugins: [ + postcssPrivateRule(/* pluginOptions */) + ] +} +``` + +## PostCSS Load Config + +If your framework/CLI supports [`postcss-load-config`](https://github.com/postcss/postcss-load-config). + +```bash +npm install @csstools/postcss-private-rule --save-dev +``` + +`package.json`: + +```json +{ + "postcss": { + "plugins": { + "@csstools/postcss-private-rule": {} + } + } +} +``` + +`.postcssrc.json`: + +```json +{ + "plugins": { + "@csstools/postcss-private-rule": {} + } +} +``` + +_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-private-rule --save-dev +``` + +Use [PostCSS Private Rule] 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-private-rule", + { + // 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-private-rule --save-dev +``` + +Use [PostCSS Private Rule] in your `postcss.config.json` file: + +```json +{ + "plugins": [ + "@csstools/postcss-private-rule" + ] +} +``` + +```json5 +{ + "plugins": [ + [ + "@csstools/postcss-private-rule", + { + // Optionally add plugin options + } + ] + ] +} +``` + +## Gulp + +Add [Gulp PostCSS] to your project: + +```bash +npm install gulp-postcss @csstools/postcss-private-rule --save-dev +``` + +Use [PostCSS Private Rule] in your Gulpfile: + +```js +const postcss = require('gulp-postcss'); +const postcssPrivateRule = require('@csstools/postcss-private-rule'); + +gulp.task('css', function () { + var plugins = [ + postcssPrivateRule(/* 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-private-rule --save-dev +``` + +Use [PostCSS Private Rule] in your Gruntfile: + +```js +const postcssPrivateRule = require('@csstools/postcss-private-rule'); + +grunt.loadNpmTasks('grunt-postcss'); + +grunt.initConfig({ + postcss: { + options: { + processors: [ + postcssPrivateRule(/* 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 Private Rule]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-private-rule +[Next.js]: https://nextjs.org diff --git a/plugins/postcss-private-rule/LICENSE.md b/plugins/postcss-private-rule/LICENSE.md new file mode 100644 index 0000000000..e8ae93b9f9 --- /dev/null +++ b/plugins/postcss-private-rule/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-private-rule/README.md b/plugins/postcss-private-rule/README.md new file mode 100644 index 0000000000..1d7f0f254b --- /dev/null +++ b/plugins/postcss-private-rule/README.md @@ -0,0 +1,78 @@ +# PostCSS Private Rule [PostCSS Logo][PostCSS] + +[npm version][npm-url] [Build Status][cli-url] [Discord][discord]

[Baseline Status][css-url] [CSS Standard Status][css-url] + +```bash +npm install @csstools/postcss-private-rule --save-dev +``` + +[PostCSS Private Rule] lets you declare private custom properties following the [CSS Mixins Specification]. + +```css +section { + @private { + --size: 2px; + } + + &, & > h1 { + border-width: var(--size); + } +} + +section > h1 { + --size: 4px; +} + +/* becomes */ + +section { + --_csstools-p-c9ee5fbd-0--size: 2px; + + &, & > h1 { + border-width: var(--_csstools-p-c9ee5fbd-0--size); + } +} + +section > h1 { + --size: 4px; +} +``` + +## Usage + +Add [PostCSS Private Rule] to your project: + +```bash +npm install postcss @csstools/postcss-private-rule --save-dev +``` + +Use it as a [PostCSS] plugin: + +```js +const postcss = require('postcss'); +const postcssPrivateRule = require('@csstools/postcss-private-rule'); + +postcss([ + postcssPrivateRule(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +[PostCSS Private Rule] 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) + +[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test +[css-url]: https://cssdb.org/#private-rule +[discord]: https://discord.gg/bUadyRwkJS +[npm-url]: https://www.npmjs.com/package/@csstools/postcss-private-rule + +[PostCSS]: https://github.com/postcss/postcss +[PostCSS Private Rule]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-private-rule +[CSS Mixins Specification]: https://drafts.csswg.org/css-mixins-1/#private diff --git a/plugins/postcss-private-rule/api-extractor.json b/plugins/postcss-private-rule/api-extractor.json new file mode 100644 index 0000000000..42058be517 --- /dev/null +++ b/plugins/postcss-private-rule/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-private-rule/dist/index.d.ts b/plugins/postcss-private-rule/dist/index.d.ts new file mode 100644 index 0000000000..dcec0ea20e --- /dev/null +++ b/plugins/postcss-private-rule/dist/index.d.ts @@ -0,0 +1,20 @@ +import type { PluginCreator } from 'postcss'; + +declare const creator: PluginCreator; +export default creator; +export { creator as 'module.exports' } + +export declare const HAS_VAR_OR_IF_FUNCTION_REGEX: RegExp; + +export declare const IS_CONTAINER_REGEX: RegExp; + +export declare const IS_IF_FUNCTION_REGEX: RegExp; + +export declare const IS_STYLE_FUNCTION_REGEX: RegExp; + +export declare const IS_VAR_FUNCTION_REGEX: RegExp; + +/** postcss-private-rule plugin options */ +export declare type pluginOptions = never; + +export { } diff --git a/plugins/postcss-private-rule/dist/index.mjs b/plugins/postcss-private-rule/dist/index.mjs new file mode 100644 index 0000000000..dde3909a35 --- /dev/null +++ b/plugins/postcss-private-rule/dist/index.mjs @@ -0,0 +1 @@ +import e from"node:crypto";import{tokenize as t,isTokenIdent as r,mutateIdent as a}from"@csstools/css-tokenizer";import{parseListOfComponentValues as n,walk as s,isFunctionNode as o,isWhiteSpaceOrCommentNode as l,isTokenNode as u,stringify as i}from"@csstools/css-parser-algorithms";const p=/^(container|layer|media|scope|starting-style|supports)$/i,c=/^private$/i,f=/^keyframes$/i;function findStyleRule(e){return!!e.parent&&("rule"===e.parent.type?("atrule"!==e.parent.parent?.type||!f.test(e.parent.parent?.name))&&e.parent:!("atrule"!==e.parent.type||!p.test(e.parent.name))&&findStyleRule(e.parent))}function findPrivateRule(e){return"atrule"===e.parent?.type&&(p.test(e.parent.name)?findPrivateRule(e.parent):!!c.test(e.parent.name)&&e.parent)}const v=/\b(var|if)\(/i,m=/^container$/i,d=/^if$/i,$=/^style$/i,g=/^var$/i,creator=()=>({postcssPlugin:"postcss-private-rule",prepare(){const p=new Map;let f=0;const propertyNamePrefix=t=>{const r=p.get(t);if(r)return r;const a=e.createHash("md5");a.update(t.source?.input.from??"","utf8");const n=`--_csstools-p-${a.digest("hex").slice(0,8)}-${f.toString(16)}`;return f++,p.set(t,n),n};return{postcssPlugin:"postcss-private-rule",Once(e){e.walkAtRules(c,e=>{const p=findStyleRule(e);if(!p)return;if(!e.nodes?.length)return;const c=propertyNamePrefix(p);e.walk(t=>{"decl"===t.type&&t.variable&&t.prop.startsWith("--")?t.prop=`${c}${t.prop}`:"atrule"===t.type&&findPrivateRule(t)===e||t.remove()}),e.nodes.forEach(t=>{e.before(t.clone())}),e.remove(),p.walkDecls(e=>{if(!v.test(e.value))return;const p=n(t({css:e.value}));s(p,e=>{if(o(e.node)){if(g.test(e.node.getName())){const t=e.node.value;for(let e=0;e{if(!m.test(e.name))return;if(!e.params.includes("--"))return;const p=n(t({css:e.params}));s(p,e=>{if(!o(e.node))return;if(!$.test(e.node.getName()))return;const t=e.node.value;for(let e=0;e + + + + + + + + + + + + + + + + +
+ +[] lets you declare private custom properties following the [CSS Mixins Specification]. + +```css + + +/* becomes */ + + +``` + + + + + + +[CSS Mixins Specification]: diff --git a/plugins/postcss-private-rule/package.json b/plugins/postcss-private-rule/package.json new file mode 100644 index 0000000000..0cd9282b3d --- /dev/null +++ b/plugins/postcss-private-rule/package.json @@ -0,0 +1,83 @@ +{ + "name": "@csstools/postcss-private-rule", + "description": "Declare private custom properties", + "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-private-rule#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/csstools/postcss-plugins.git", + "directory": "plugins/postcss-private-rule" + }, + "bugs": "https://github.com/csstools/postcss-plugins/issues", + "keywords": [ + "custom properties", + "fallback", + "postcss-plugin", + "private" + ], + "csstools": { + "cssdbId": "private-rule", + "exportName": "postcssPrivateRule", + "humanReadableName": "PostCSS Private Rule", + "specUrl": "https://drafts.csswg.org/css-mixins-1/#private" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/plugins/postcss-private-rule/src/index.ts b/plugins/postcss-private-rule/src/index.ts new file mode 100644 index 0000000000..d8bcbf8ad6 --- /dev/null +++ b/plugins/postcss-private-rule/src/index.ts @@ -0,0 +1,175 @@ +import type { Plugin, PluginCreator, Rule } from 'postcss'; +import crypto from 'node:crypto'; +import { findPrivateRule, findStyleRule, IS_PRIVATE_RULE_REGEX } from './valid-atrules'; +import { isTokenIdent, mutateIdent, tokenize } from '@csstools/css-tokenizer'; +import { isFunctionNode, isTokenNode, isWhiteSpaceOrCommentNode, parseListOfComponentValues, stringify, walk } from '@csstools/css-parser-algorithms'; + +export const HAS_VAR_OR_IF_FUNCTION_REGEX = /\b(var|if)\(/i; +export const IS_CONTAINER_REGEX = /^container$/i; +export const IS_IF_FUNCTION_REGEX = /^if$/i; +export const IS_STYLE_FUNCTION_REGEX = /^style$/i; +export const IS_VAR_FUNCTION_REGEX = /^var$/i; + +/** postcss-private-rule plugin options */ +export type pluginOptions = never; + +const creator: PluginCreator = () => { + return { + postcssPlugin: 'postcss-private-rule', + prepare(): Plugin { + const privatePropertyNames: WeakMap = new Map(); + + let counter = 0; + const propertyNamePrefix = (rule: Rule): string => { + const existing = privatePropertyNames.get(rule); + if (existing) { + return existing; + } + + const hash = crypto.createHash('md5'); + hash.update(rule.source?.input.from ?? '', 'utf8'); + + const prefix = `--_csstools-p-${hash.digest('hex').slice(0, 8) }-${counter.toString(16)}`; + counter++; + + privatePropertyNames.set(rule, prefix); + + return prefix; + }; + + return { + postcssPlugin: 'postcss-private-rule', + Once(root): void { + root.walkAtRules(IS_PRIVATE_RULE_REGEX, (atRule) => { + const styleRule = findStyleRule(atRule); + if (!styleRule) { + return; + } + + if (!atRule.nodes?.length) { + return; + } + + const prefix = propertyNamePrefix(styleRule); + + { + atRule.walk((node) => { + if (node.type === 'decl' && node.variable && node.prop.startsWith('--')) { + node.prop = `${prefix}${node.prop}`; + + return; + } + + if (node.type === 'atrule' && findPrivateRule(node) === atRule) { + return; + } + + node.remove(); + }); + + atRule.nodes.forEach((node) => { + atRule.before(node.clone()); + }); + + atRule.remove(); + } + + { + styleRule.walkDecls((decl) => { + if (!HAS_VAR_OR_IF_FUNCTION_REGEX.test(decl.value)) return; + + const componentValues = parseListOfComponentValues(tokenize({ css: decl.value })); + + walk(componentValues, (entry) => { + if (!isFunctionNode(entry.node)) { + return; + } + + if (IS_VAR_FUNCTION_REGEX.test(entry.node.getName())) { + const args = entry.node.value; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (isWhiteSpaceOrCommentNode(arg)) continue; + + if (!isTokenNode(arg) || !isTokenIdent(arg.value) || !arg.value[4].value.startsWith('--')) break; + + mutateIdent(arg.value, `${prefix}${arg.value[4].value}`); + break; + } + + return; + } + + if (IS_IF_FUNCTION_REGEX.test(entry.node.getName())) { + const args = entry.node.value; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (isWhiteSpaceOrCommentNode(arg)) continue; + + if (!isFunctionNode(arg) || !IS_STYLE_FUNCTION_REGEX.test(arg.getName())) break; + + const childArgs = arg.value; + for (let j = 0; j < childArgs.length; j++) { + const childArg = childArgs[j]; + + if (isWhiteSpaceOrCommentNode(childArg)) continue; + + if (!isTokenNode(childArg) || !isTokenIdent(childArg.value) || !childArg.value[4].value.startsWith('--')) break; + + mutateIdent(childArg.value, `${prefix}${childArg.value[4].value}`); + break; + } + } + } + }); + + decl.value = stringify([componentValues]); + }); + } + + { + styleRule.walkAtRules((nestedAtRule) => { + if (!IS_CONTAINER_REGEX.test(nestedAtRule.name)) return; + + if (!nestedAtRule.params.includes('--')) return; + + const componentValues = parseListOfComponentValues(tokenize({ css: nestedAtRule.params })); + + walk(componentValues, (entry) => { + if (!isFunctionNode(entry.node)) return; + + if (!IS_STYLE_FUNCTION_REGEX.test(entry.node.getName())) return; + + const args = entry.node.value; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (isWhiteSpaceOrCommentNode(arg)) continue; + + if (!isTokenNode(arg) || !isTokenIdent(arg.value) || !arg.value[4].value.startsWith('--')) break; + + mutateIdent(arg.value, `${prefix}${arg.value[4].value}`); + break; + } + }); + + nestedAtRule.params = stringify([componentValues]); + }); + } + + // TODO: + // substitute custom prop names in: + // - if() + }); + }, + }; + } + }; +}; + +creator.postcss = true; + +export default creator; +export { creator as 'module.exports' }; diff --git a/plugins/postcss-private-rule/src/valid-atrules.ts b/plugins/postcss-private-rule/src/valid-atrules.ts new file mode 100644 index 0000000000..ab846a2ee3 --- /dev/null +++ b/plugins/postcss-private-rule/src/valid-atrules.ts @@ -0,0 +1,44 @@ +import type { AtRule, Rule } from "postcss"; + +const IS_NESTING_GROUP_RULE_REGEX = /^(container|layer|media|scope|starting-style|supports)$/i; + +export const IS_PRIVATE_RULE_REGEX = /^private$/i; + +const IS_KEYFRAMES_RULE_REGEX = /^keyframes$/i; + +export function findStyleRule(atRule: AtRule): false|Rule { + if (!atRule.parent) { + return false; + } + + if (atRule.parent.type === 'rule') { + if (atRule.parent.parent?.type === 'atrule' && IS_KEYFRAMES_RULE_REGEX.test(atRule.parent.parent?.name)) { + return false; + } + + return atRule.parent; + } + + if (atRule.parent.type === 'atrule' && IS_NESTING_GROUP_RULE_REGEX.test(atRule.parent.name)) { + return findStyleRule(atRule.parent); + } + + return false; +} + + +export function findPrivateRule(atRule: AtRule): false | AtRule { + if (atRule.parent?.type !== 'atrule') { + return false; + } + + if (IS_NESTING_GROUP_RULE_REGEX.test(atRule.parent.name)) { + return findPrivateRule(atRule.parent); + } + + if (IS_PRIVATE_RULE_REGEX.test(atRule.parent.name)) { + return atRule.parent; + } + + return false; +} diff --git a/plugins/postcss-private-rule/test/_import.mjs b/plugins/postcss-private-rule/test/_import.mjs new file mode 100644 index 0000000000..b695f3d333 --- /dev/null +++ b/plugins/postcss-private-rule/test/_import.mjs @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import plugin from '@csstools/postcss-private-rule'; + +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-private-rule/test/_require.cjs b/plugins/postcss-private-rule/test/_require.cjs new file mode 100644 index 0000000000..3f257495aa --- /dev/null +++ b/plugins/postcss-private-rule/test/_require.cjs @@ -0,0 +1,9 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); +const plugin = require('@csstools/postcss-private-rule'); + +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-private-rule/test/_tape.mjs b/plugins/postcss-private-rule/test/_tape.mjs new file mode 100644 index 0000000000..d16cd1d118 --- /dev/null +++ b/plugins/postcss-private-rule/test/_tape.mjs @@ -0,0 +1,11 @@ +import { postcssTape } from '@csstools/postcss-tape'; +import plugin from '@csstools/postcss-private-rule'; + +postcssTape(plugin)({ + basic: { + message: 'supports basic usage', + }, + 'examples/example': { + message: 'minimal example', + }, +}); diff --git a/plugins/postcss-private-rule/test/basic.css b/plugins/postcss-private-rule/test/basic.css new file mode 100644 index 0000000000..ba0558faaa --- /dev/null +++ b/plugins/postcss-private-rule/test/basic.css @@ -0,0 +1,145 @@ +section { + @private { + --size: 2px; + } + + @private { + --color: red; + } + + &, & > h1, & > h1 > small { + border-width: var(--size); + border-color: var(--color); + } +} + +section > h1 { + --size: 4px; +} + +section > h1 > small { + --size: 3px; +} + +.public-before-private { + --alpha: 1px; + + @private { + --alpha: 2px; + } + + --beta: var(--alpha); +} + +.private-before-public { + @private { + --alpha: 2px; + } + + --alpha: 3px; + + --beta: var(--alpha); +} + +.single-private { + @private { + --alpha: var(--beta); + } +} + +.two-private { + @private { + --alpha: 1; + --beta: 2; + } +} + +.container-query { + @private { + --alpha: private; + } + + --alpha: public; + + @container style(--alpha: private) { + --bar: cyan; + } +} + +.container-query-and-nesting { + @private { + --alpha: private; + } + + --alpha: public; + + & .bar { + @container style(--alpha: private) { + --bar: cyan; + } + } +} + +.if-function { + @private { + --alpha: private; + } + + --alpha: public; + + --bar: if( + style(--alpha: private): cyan; + else: orange + ); +} + +.invalid-private-contents{ + @private { + --alpha: 2px; + + &+& { + --alpha: 3px; + } + } + + --beta: var(--alpha); +} + +.conditional-private { + @private { + --alpha: 2px; + + @media screen { + --alpha: 3px; + } + } + + --beta: var(--alpha); +} + +.conditional-private-use { + @private { + --alpha: 2px; + } + + @media screen { + --beta: var(--alpha); + } +} + +@private { + --invalid-position: 2px; +} + +:root { + --something: var(--invalid-position); +} + +@keyframes slide-right { + from { + @private { + --alpha: 2px; + } + margin-left: var(--alpha); + } +} diff --git a/plugins/postcss-private-rule/test/basic.expect.css b/plugins/postcss-private-rule/test/basic.expect.css new file mode 100644 index 0000000000..0dd7f5bcb5 --- /dev/null +++ b/plugins/postcss-private-rule/test/basic.expect.css @@ -0,0 +1,115 @@ +section { + --_csstools-p-9fc6ed48-0--size: 2px; + --_csstools-p-9fc6ed48-0--color: red; + + &, & > h1, & > h1 > small { + border-width: var(--_csstools-p-9fc6ed48-0--_csstools-p-9fc6ed48-0--size); + border-color: var(--_csstools-p-9fc6ed48-0--_csstools-p-9fc6ed48-0--color); + } +} + +section > h1 { + --size: 4px; +} + +section > h1 > small { + --size: 3px; +} + +.public-before-private { + --alpha: 1px; + --_csstools-p-9fc6ed48-1--alpha: 2px; + + --beta: var(--_csstools-p-9fc6ed48-1--alpha); +} + +.private-before-public { + --_csstools-p-9fc6ed48-2--alpha: 2px; + + --alpha: 3px; + + --beta: var(--_csstools-p-9fc6ed48-2--alpha); +} + +.single-private { + --_csstools-p-9fc6ed48-3--alpha: var(--_csstools-p-9fc6ed48-3--beta) +} + +.two-private { + --_csstools-p-9fc6ed48-4--alpha: 1; + --_csstools-p-9fc6ed48-4--beta: 2 +} + +.container-query { + --_csstools-p-9fc6ed48-5--alpha: private; + + --alpha: public; + + @container style(--_csstools-p-9fc6ed48-5--alpha: private) { + --bar: cyan; + } +} + +.container-query-and-nesting { + --_csstools-p-9fc6ed48-6--alpha: private; + + --alpha: public; + + & .bar { + @container style(--_csstools-p-9fc6ed48-6--alpha: private) { + --bar: cyan; + } + } +} + +.if-function { + --_csstools-p-9fc6ed48-7--alpha: private; + + --alpha: public; + + --bar: if( + style(--_csstools-p-9fc6ed48-7--alpha: private): cyan; + else: orange + ); +} + +.invalid-private-contents{ + --_csstools-p-9fc6ed48-8--alpha: 2px; + + --beta: var(--_csstools-p-9fc6ed48-8--alpha); +} + +.conditional-private { + --_csstools-p-9fc6ed48-9--alpha: 2px; + + @media screen { + --_csstools-p-9fc6ed48-9--alpha: 3px; + } + + --beta: var(--_csstools-p-9fc6ed48-9--alpha); +} + +.conditional-private-use { + --_csstools-p-9fc6ed48-a--alpha: 2px; + + @media screen { + --beta: var(--_csstools-p-9fc6ed48-a--alpha); + } +} + +@private { + --invalid-position: 2px; +} + +:root { + --something: var(--invalid-position); +} + +@keyframes slide-right { + from { + @private { + --alpha: 2px; + } + margin-left: var(--alpha); + } +} diff --git a/plugins/postcss-private-rule/test/examples/example.css b/plugins/postcss-private-rule/test/examples/example.css new file mode 100644 index 0000000000..32cfe86f7f --- /dev/null +++ b/plugins/postcss-private-rule/test/examples/example.css @@ -0,0 +1,13 @@ +section { + @private { + --size: 2px; + } + + &, & > h1 { + border-width: var(--size); + } +} + +section > h1 { + --size: 4px; +} diff --git a/plugins/postcss-private-rule/test/examples/example.expect.css b/plugins/postcss-private-rule/test/examples/example.expect.css new file mode 100644 index 0000000000..95c2525d96 --- /dev/null +++ b/plugins/postcss-private-rule/test/examples/example.expect.css @@ -0,0 +1,11 @@ +section { + --_csstools-p-c9ee5fbd-0--size: 2px; + + &, & > h1 { + border-width: var(--_csstools-p-c9ee5fbd-0--size); + } +} + +section > h1 { + --size: 4px; +} diff --git a/plugins/postcss-private-rule/tsconfig.json b/plugins/postcss-private-rule/tsconfig.json new file mode 100644 index 0000000000..24dab9cc3d --- /dev/null +++ b/plugins/postcss-private-rule/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": ".", + "rootDir": "./src", + "strict": true + }, + "include": ["./src/**/*"], + "exclude": ["dist"] +} diff --git a/rollup/configs/externals.mjs b/rollup/configs/externals.mjs index f76da06a94..5730e53d82 100644 --- a/rollup/configs/externals.mjs +++ b/rollup/configs/externals.mjs @@ -58,6 +58,7 @@ export const externalsForCLI = [ '@csstools/postcss-normalize-display-values', '@csstools/postcss-oklab-function', '@csstools/postcss-position-area-property', + '@csstools/postcss-private-rule', '@csstools/postcss-progressive-custom-properties', '@csstools/postcss-property-rule-optional-descriptors', '@csstools/postcss-property-rule-prelude-list', @@ -174,6 +175,7 @@ export const externalsForPlugin = [ '@csstools/postcss-normalize-display-values', '@csstools/postcss-oklab-function', '@csstools/postcss-position-area-property', + '@csstools/postcss-private-rule', '@csstools/postcss-progressive-custom-properties', '@csstools/postcss-property-rule-optional-descriptors', '@csstools/postcss-property-rule-prelude-list', From 6423e962c10ee741eb397c1e31f68332528b5e01 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Sun, 30 Aug 2026 18:10:41 +0200 Subject: [PATCH 2/5] fix --- plugins/postcss-private-rule/dist/index.d.ts | 10 ---------- plugins/postcss-private-rule/dist/index.mjs | 2 +- plugins/postcss-private-rule/src/index.ts | 10 +++++----- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/plugins/postcss-private-rule/dist/index.d.ts b/plugins/postcss-private-rule/dist/index.d.ts index dcec0ea20e..54b07f33d4 100644 --- a/plugins/postcss-private-rule/dist/index.d.ts +++ b/plugins/postcss-private-rule/dist/index.d.ts @@ -4,16 +4,6 @@ declare const creator: PluginCreator; export default creator; export { creator as 'module.exports' } -export declare const HAS_VAR_OR_IF_FUNCTION_REGEX: RegExp; - -export declare const IS_CONTAINER_REGEX: RegExp; - -export declare const IS_IF_FUNCTION_REGEX: RegExp; - -export declare const IS_STYLE_FUNCTION_REGEX: RegExp; - -export declare const IS_VAR_FUNCTION_REGEX: RegExp; - /** postcss-private-rule plugin options */ export declare type pluginOptions = never; diff --git a/plugins/postcss-private-rule/dist/index.mjs b/plugins/postcss-private-rule/dist/index.mjs index dde3909a35..e6ef068741 100644 --- a/plugins/postcss-private-rule/dist/index.mjs +++ b/plugins/postcss-private-rule/dist/index.mjs @@ -1 +1 @@ -import e from"node:crypto";import{tokenize as t,isTokenIdent as r,mutateIdent as a}from"@csstools/css-tokenizer";import{parseListOfComponentValues as n,walk as s,isFunctionNode as o,isWhiteSpaceOrCommentNode as l,isTokenNode as u,stringify as i}from"@csstools/css-parser-algorithms";const p=/^(container|layer|media|scope|starting-style|supports)$/i,c=/^private$/i,f=/^keyframes$/i;function findStyleRule(e){return!!e.parent&&("rule"===e.parent.type?("atrule"!==e.parent.parent?.type||!f.test(e.parent.parent?.name))&&e.parent:!("atrule"!==e.parent.type||!p.test(e.parent.name))&&findStyleRule(e.parent))}function findPrivateRule(e){return"atrule"===e.parent?.type&&(p.test(e.parent.name)?findPrivateRule(e.parent):!!c.test(e.parent.name)&&e.parent)}const v=/\b(var|if)\(/i,m=/^container$/i,d=/^if$/i,$=/^style$/i,g=/^var$/i,creator=()=>({postcssPlugin:"postcss-private-rule",prepare(){const p=new Map;let f=0;const propertyNamePrefix=t=>{const r=p.get(t);if(r)return r;const a=e.createHash("md5");a.update(t.source?.input.from??"","utf8");const n=`--_csstools-p-${a.digest("hex").slice(0,8)}-${f.toString(16)}`;return f++,p.set(t,n),n};return{postcssPlugin:"postcss-private-rule",Once(e){e.walkAtRules(c,e=>{const p=findStyleRule(e);if(!p)return;if(!e.nodes?.length)return;const c=propertyNamePrefix(p);e.walk(t=>{"decl"===t.type&&t.variable&&t.prop.startsWith("--")?t.prop=`${c}${t.prop}`:"atrule"===t.type&&findPrivateRule(t)===e||t.remove()}),e.nodes.forEach(t=>{e.before(t.clone())}),e.remove(),p.walkDecls(e=>{if(!v.test(e.value))return;const p=n(t({css:e.value}));s(p,e=>{if(o(e.node)){if(g.test(e.node.getName())){const t=e.node.value;for(let e=0;e{if(!m.test(e.name))return;if(!e.params.includes("--"))return;const p=n(t({css:e.params}));s(p,e=>{if(!o(e.node))return;if(!$.test(e.node.getName()))return;const t=e.node.value;for(let e=0;e({postcssPlugin:"postcss-private-rule",prepare(){const p=new Map;let f=0;const propertyNamePrefix=t=>{const r=p.get(t);if(r)return r;const a=e.createHash("md5");a.update(t.source?.input.from??"","utf8");const n=`--_csstools-p-${a.digest("hex").slice(0,8)}-${f.toString(16)}`;return f++,p.set(t,n),n};return{postcssPlugin:"postcss-private-rule",Once(e){e.walkAtRules(c,e=>{const p=findStyleRule(e);if(!p)return;if(!e.nodes?.length)return;const c=propertyNamePrefix(p);e.walk(t=>{"decl"===t.type&&t.variable&&t.prop.startsWith("--")?t.prop=`${c}${t.prop}`:"atrule"===t.type&&findPrivateRule(t)===e||t.remove()}),e.nodes.forEach(t=>{e.before(t.clone())}),e.remove(),p.walkDecls(e=>{if(!v.test(e.value))return;const p=n(t({css:e.value}));s(p,e=>{if(o(e.node)){if(g.test(e.node.getName())){const t=e.node.value;for(let e=0;e{if(!m.test(e.name))return;if(!e.params.includes("--"))return;const p=n(t({css:e.params}));s(p,e=>{if(!o(e.node))return;if(!$.test(e.node.getName()))return;const t=e.node.value;for(let e=0;e Date: Sun, 30 Aug 2026 18:21:16 +0200 Subject: [PATCH 3/5] more fixes --- plugins/postcss-private-rule/dist/index.mjs | 2 +- plugins/postcss-private-rule/src/index.ts | 15 ++++-- plugins/postcss-private-rule/test/basic.css | 46 ++++++++++++++++++- .../test/basic.expect.css | 44 ++++++++++++++++-- 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/plugins/postcss-private-rule/dist/index.mjs b/plugins/postcss-private-rule/dist/index.mjs index e6ef068741..909425b3f8 100644 --- a/plugins/postcss-private-rule/dist/index.mjs +++ b/plugins/postcss-private-rule/dist/index.mjs @@ -1 +1 @@ -import e from"node:crypto";import{tokenize as t,isTokenIdent as r,mutateIdent as a}from"@csstools/css-tokenizer";import{parseListOfComponentValues as n,walk as s,isFunctionNode as o,isWhiteSpaceOrCommentNode as l,isTokenNode as u,stringify as i}from"@csstools/css-parser-algorithms";const p=/^(container|layer|media|scope|starting-style|supports)$/i,c=/^private$/i,f=/^keyframes$/i;function findStyleRule(e){return!!e.parent&&("rule"===e.parent.type?("atrule"!==e.parent.parent?.type||!f.test(e.parent.parent?.name))&&e.parent:!("atrule"!==e.parent.type||!p.test(e.parent.name))&&findStyleRule(e.parent))}function findPrivateRule(e){return"atrule"===e.parent?.type&&(p.test(e.parent.name)?findPrivateRule(e.parent):!!c.test(e.parent.name)&&e.parent)}const v=/\b(var|if)\(/i,m=/^container$/i,d=/^if$/i,$=/^style$/i,g=/^var$/i,creator=()=>({postcssPlugin:"postcss-private-rule",prepare(){const p=new Map;let f=0;const propertyNamePrefix=t=>{const r=p.get(t);if(r)return r;const a=e.createHash("md5");a.update(t.source?.input.from??"","utf8");const n=`--_csstools-p-${a.digest("hex").slice(0,8)}-${f.toString(16)}`;return f++,p.set(t,n),n};return{postcssPlugin:"postcss-private-rule",Once(e){e.walkAtRules(c,e=>{const p=findStyleRule(e);if(!p)return;if(!e.nodes?.length)return;const c=propertyNamePrefix(p);e.walk(t=>{"decl"===t.type&&t.variable&&t.prop.startsWith("--")?t.prop=`${c}${t.prop}`:"atrule"===t.type&&findPrivateRule(t)===e||t.remove()}),e.nodes.forEach(t=>{e.before(t.clone())}),e.remove(),p.walkDecls(e=>{if(!v.test(e.value))return;const p=n(t({css:e.value}));s(p,e=>{if(o(e.node)){if(g.test(e.node.getName())){const t=e.node.value;for(let e=0;e{if(!m.test(e.name))return;if(!e.params.includes("--"))return;const p=n(t({css:e.params}));s(p,e=>{if(!o(e.node))return;if(!$.test(e.node.getName()))return;const t=e.node.value;for(let e=0;e({postcssPlugin:"postcss-private-rule",prepare(){const p=new Map,f=new Set;let h=0;const propertyNamePrefix=t=>{const a=p.get(t);if(a)return a;const r=e.createHash("md5");r.update(t.source?.input.from??"","utf8");const s=`--_csstools-p-${r.digest("hex").slice(0,8)}-${h.toString(16)}`;return h++,p.set(t,s),s};return{postcssPlugin:"postcss-private-rule",Once(e){e.walkAtRules(c,e=>{const p=findStyleRule(e);if(!p)return;if(!e.nodes?.length)return;const c=propertyNamePrefix(p);e.walk(t=>{if("decl"===t.type&&t.variable&&t.prop.startsWith("--"))return f.add(t.prop),void(t.prop=`${c}${t.prop}`);"atrule"===t.type&&findPrivateRule(t)===e||t.remove()}),e.nodes.forEach(t=>{e.before(t.clone())}),e.remove(),p.walkDecls(e=>{if(!v.test(e.value))return;const p=s(t({css:e.value}));n(p,e=>{if(l(e.node)){if(g.test(e.node.getName())){const t=e.node.value;for(let e=0;e{if(!d.test(e.name))return;if(!e.params.includes("--"))return;const p=s(t({css:e.params}));n(p,e=>{if(!l(e.node))return;if(!$.test(e.node.getName()))return;const t=e.node.value;for(let e=0;e = () => { return { postcssPlugin: 'postcss-private-rule', prepare(): Plugin { - const privatePropertyNames: WeakMap = new Map(); + const privatePropertyNamePrefixes: WeakMap = new Map(); + const privatePropertyNames: Set = new Set(); let counter = 0; const propertyNamePrefix = (rule: Rule): string => { - const existing = privatePropertyNames.get(rule); + const existing = privatePropertyNamePrefixes.get(rule); if (existing) { return existing; } @@ -32,7 +33,7 @@ const creator: PluginCreator = () => { const prefix = `--_csstools-p-${hash.digest('hex').slice(0, 8) }-${counter.toString(16)}`; counter++; - privatePropertyNames.set(rule, prefix); + privatePropertyNamePrefixes.set(rule, prefix); return prefix; }; @@ -55,6 +56,8 @@ const creator: PluginCreator = () => { { atRule.walk((node) => { if (node.type === 'decl' && node.variable && node.prop.startsWith('--')) { + privatePropertyNames.add(node.prop); + node.prop = `${prefix}${node.prop}`; return; @@ -94,6 +97,8 @@ const creator: PluginCreator = () => { if (!isTokenNode(arg) || !isTokenIdent(arg.value) || !arg.value[4].value.startsWith('--')) break; + if (!privatePropertyNames.has(arg.value[4].value)) break; + mutateIdent(arg.value, `${prefix}${arg.value[4].value}`); break; } @@ -118,6 +123,8 @@ const creator: PluginCreator = () => { if (!isTokenNode(childArg) || !isTokenIdent(childArg.value) || !childArg.value[4].value.startsWith('--')) break; + if (!privatePropertyNames.has(childArg.value[4].value)) break; + mutateIdent(childArg.value, `${prefix}${childArg.value[4].value}`); break; } @@ -150,6 +157,8 @@ const creator: PluginCreator = () => { if (!isTokenNode(arg) || !isTokenIdent(arg.value) || !arg.value[4].value.startsWith('--')) break; + if (!privatePropertyNames.has(arg.value[4].value)) break; + mutateIdent(arg.value, `${prefix}${arg.value[4].value}`); break; } diff --git a/plugins/postcss-private-rule/test/basic.css b/plugins/postcss-private-rule/test/basic.css index ba0558faaa..b1b6afa2bf 100644 --- a/plugins/postcss-private-rule/test/basic.css +++ b/plugins/postcss-private-rule/test/basic.css @@ -89,7 +89,12 @@ section > h1 > small { --bar: if( style(--alpha: private): cyan; - else: orange + else: orange; + ); + + --baz: if( + style(--something: var(--alpha)): cyan; + else: orange; ); } @@ -143,3 +148,42 @@ section > h1 > small { margin-left: var(--alpha); } } + +.ignore { + @private { + --alpha: 2px; + } + + --beta: var(--foo, --alpha); +} + +.ignore { + @private { + --alpha: 2px; + } + + --beta: --alpha; +} + +.ignore { + @private { + --alpha: 2px; + } + + --bar: if( + style(--foo: --alpha): cyan; + else: orange; + ); +} + +.ignore { + @private { + --alpha: 2px; + } + + & .bar { + @container style(--something: --alpha) { + --bar: cyan; + } + } +} diff --git a/plugins/postcss-private-rule/test/basic.expect.css b/plugins/postcss-private-rule/test/basic.expect.css index 0dd7f5bcb5..b70e6d295f 100644 --- a/plugins/postcss-private-rule/test/basic.expect.css +++ b/plugins/postcss-private-rule/test/basic.expect.css @@ -3,8 +3,8 @@ section { --_csstools-p-9fc6ed48-0--color: red; &, & > h1, & > h1 > small { - border-width: var(--_csstools-p-9fc6ed48-0--_csstools-p-9fc6ed48-0--size); - border-color: var(--_csstools-p-9fc6ed48-0--_csstools-p-9fc6ed48-0--color); + border-width: var(--_csstools-p-9fc6ed48-0--size); + border-color: var(--_csstools-p-9fc6ed48-0--color); } } @@ -32,7 +32,7 @@ section > h1 > small { } .single-private { - --_csstools-p-9fc6ed48-3--alpha: var(--_csstools-p-9fc6ed48-3--beta) + --_csstools-p-9fc6ed48-3--alpha: var(--beta) } .two-private { @@ -69,7 +69,12 @@ section > h1 > small { --bar: if( style(--_csstools-p-9fc6ed48-7--alpha: private): cyan; - else: orange + else: orange; + ); + + --baz: if( + style(--something: var(--_csstools-p-9fc6ed48-7--alpha)): cyan; + else: orange; ); } @@ -113,3 +118,34 @@ section > h1 > small { margin-left: var(--alpha); } } + +.ignore { + --_csstools-p-9fc6ed48-b--alpha: 2px; + + --beta: var(--foo, --alpha); +} + +.ignore { + --_csstools-p-9fc6ed48-c--alpha: 2px; + + --beta: --alpha; +} + +.ignore { + --_csstools-p-9fc6ed48-d--alpha: 2px; + + --bar: if( + style(--foo: --alpha): cyan; + else: orange; + ); +} + +.ignore { + --_csstools-p-9fc6ed48-e--alpha: 2px; + + & .bar { + @container style(--something: --alpha) { + --bar: cyan; + } + } +} From e3c7184f24db6ee10c232b2b2779fbffae35ad13 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Sun, 30 Aug 2026 18:29:05 +0200 Subject: [PATCH 4/5] stable hashes --- plugins/postcss-private-rule/dist/index.mjs | 2 +- plugins/postcss-private-rule/src/index.ts | 15 ++++- .../test/basic.expect.css | 58 +++++++++---------- .../test/examples/example.expect.css | 4 +- 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/plugins/postcss-private-rule/dist/index.mjs b/plugins/postcss-private-rule/dist/index.mjs index 909425b3f8..3ea57ee4c4 100644 --- a/plugins/postcss-private-rule/dist/index.mjs +++ b/plugins/postcss-private-rule/dist/index.mjs @@ -1 +1 @@ -import e from"node:crypto";import{tokenize as t,isTokenIdent as a,mutateIdent as r}from"@csstools/css-tokenizer";import{parseListOfComponentValues as s,walk as n,isFunctionNode as l,isWhiteSpaceOrCommentNode as o,isTokenNode as u,stringify as i}from"@csstools/css-parser-algorithms";const p=/^(container|layer|media|scope|starting-style|supports)$/i,c=/^private$/i,f=/^keyframes$/i;function findStyleRule(e){return!!e.parent&&("rule"===e.parent.type?("atrule"!==e.parent.parent?.type||!f.test(e.parent.parent?.name))&&e.parent:!("atrule"!==e.parent.type||!p.test(e.parent.name))&&findStyleRule(e.parent))}function findPrivateRule(e){return"atrule"===e.parent?.type&&(p.test(e.parent.name)?findPrivateRule(e.parent):!!c.test(e.parent.name)&&e.parent)}const v=/\b(var|if)\(/i,d=/^container$/i,m=/^if$/i,$=/^style$/i,g=/^var$/i,creator=()=>({postcssPlugin:"postcss-private-rule",prepare(){const p=new Map,f=new Set;let h=0;const propertyNamePrefix=t=>{const a=p.get(t);if(a)return a;const r=e.createHash("md5");r.update(t.source?.input.from??"","utf8");const s=`--_csstools-p-${r.digest("hex").slice(0,8)}-${h.toString(16)}`;return h++,p.set(t,s),s};return{postcssPlugin:"postcss-private-rule",Once(e){e.walkAtRules(c,e=>{const p=findStyleRule(e);if(!p)return;if(!e.nodes?.length)return;const c=propertyNamePrefix(p);e.walk(t=>{if("decl"===t.type&&t.variable&&t.prop.startsWith("--"))return f.add(t.prop),void(t.prop=`${c}${t.prop}`);"atrule"===t.type&&findPrivateRule(t)===e||t.remove()}),e.nodes.forEach(t=>{e.before(t.clone())}),e.remove(),p.walkDecls(e=>{if(!v.test(e.value))return;const p=s(t({css:e.value}));n(p,e=>{if(l(e.node)){if(g.test(e.node.getName())){const t=e.node.value;for(let e=0;e{if(!d.test(e.name))return;if(!e.params.includes("--"))return;const p=s(t({css:e.params}));n(p,e=>{if(!l(e.node))return;if(!$.test(e.node.getName()))return;const t=e.node.value;for(let e=0;e({postcssPlugin:"postcss-private-rule",prepare(){const c=new Map,v=new Set;let k=0;const propertyNamePrefix=a=>{const r=c.get(a);if(r)return r;let s;if(a.source?.input.from){const r=e.createHash("md5");r.update(t.basename(t.dirname(a.source?.input.from))+"/"+t.basename(a.source?.input.from),"utf8"),s=r.digest("hex").slice(0,8)}else{const t=e.createHash("md5");t.update("","utf8"),s=t.digest("hex").slice(0,8)}const n=`--_csstools-p-${s}-${k.toString(16)}`;return k++,c.set(a,n),n};return{postcssPlugin:"postcss-private-rule",Once(e){e.walkAtRules(f,e=>{const t=findStyleRule(e);if(!t)return;if(!e.nodes?.length)return;const c=propertyNamePrefix(t);e.walk(t=>{if("decl"===t.type&&t.variable&&t.prop.startsWith("--"))return v.add(t.prop),void(t.prop=`${c}${t.prop}`);"atrule"===t.type&&findPrivateRule(t)===e||t.remove()}),e.nodes.forEach(t=>{e.before(t.clone())}),e.remove(),t.walkDecls(e=>{if(!m.test(e.value))return;const t=n(a({css:e.value}));o(t,e=>{if(l(e.node)){if($.test(e.node.getName())){const t=e.node.value;for(let e=0;e{if(!d.test(e.name))return;if(!e.params.includes("--"))return;const t=n(a({css:e.params}));o(t,e=>{if(!l(e.node))return;if(!g.test(e.node.getName()))return;const t=e.node.value;for(let e=0;e = () => { return existing; } - const hash = crypto.createHash('md5'); - hash.update(rule.source?.input.from ?? '', 'utf8'); + let fromHash; + if (rule.source?.input.from) { + const hash = crypto.createHash('md5'); + hash.update(path.basename(path.dirname(rule.source?.input.from)) + '/' + path.basename(rule.source?.input.from), 'utf8'); + fromHash = hash.digest('hex').slice(0, 8); + } else { + const hash = crypto.createHash('md5'); + hash.update('', 'utf8'); + fromHash = hash.digest('hex').slice(0, 8); + } - const prefix = `--_csstools-p-${hash.digest('hex').slice(0, 8) }-${counter.toString(16)}`; + const prefix = `--_csstools-p-${fromHash}-${counter.toString(16)}`; counter++; privatePropertyNamePrefixes.set(rule, prefix); diff --git a/plugins/postcss-private-rule/test/basic.expect.css b/plugins/postcss-private-rule/test/basic.expect.css index b70e6d295f..52ec30539f 100644 --- a/plugins/postcss-private-rule/test/basic.expect.css +++ b/plugins/postcss-private-rule/test/basic.expect.css @@ -1,10 +1,10 @@ section { - --_csstools-p-9fc6ed48-0--size: 2px; - --_csstools-p-9fc6ed48-0--color: red; + --_csstools-p-8a2d0276-0--size: 2px; + --_csstools-p-8a2d0276-0--color: red; &, & > h1, & > h1 > small { - border-width: var(--_csstools-p-9fc6ed48-0--size); - border-color: var(--_csstools-p-9fc6ed48-0--color); + border-width: var(--_csstools-p-8a2d0276-0--size); + border-color: var(--_csstools-p-8a2d0276-0--color); } } @@ -18,87 +18,87 @@ section > h1 > small { .public-before-private { --alpha: 1px; - --_csstools-p-9fc6ed48-1--alpha: 2px; + --_csstools-p-8a2d0276-1--alpha: 2px; - --beta: var(--_csstools-p-9fc6ed48-1--alpha); + --beta: var(--_csstools-p-8a2d0276-1--alpha); } .private-before-public { - --_csstools-p-9fc6ed48-2--alpha: 2px; + --_csstools-p-8a2d0276-2--alpha: 2px; --alpha: 3px; - --beta: var(--_csstools-p-9fc6ed48-2--alpha); + --beta: var(--_csstools-p-8a2d0276-2--alpha); } .single-private { - --_csstools-p-9fc6ed48-3--alpha: var(--beta) + --_csstools-p-8a2d0276-3--alpha: var(--beta) } .two-private { - --_csstools-p-9fc6ed48-4--alpha: 1; - --_csstools-p-9fc6ed48-4--beta: 2 + --_csstools-p-8a2d0276-4--alpha: 1; + --_csstools-p-8a2d0276-4--beta: 2 } .container-query { - --_csstools-p-9fc6ed48-5--alpha: private; + --_csstools-p-8a2d0276-5--alpha: private; --alpha: public; - @container style(--_csstools-p-9fc6ed48-5--alpha: private) { + @container style(--_csstools-p-8a2d0276-5--alpha: private) { --bar: cyan; } } .container-query-and-nesting { - --_csstools-p-9fc6ed48-6--alpha: private; + --_csstools-p-8a2d0276-6--alpha: private; --alpha: public; & .bar { - @container style(--_csstools-p-9fc6ed48-6--alpha: private) { + @container style(--_csstools-p-8a2d0276-6--alpha: private) { --bar: cyan; } } } .if-function { - --_csstools-p-9fc6ed48-7--alpha: private; + --_csstools-p-8a2d0276-7--alpha: private; --alpha: public; --bar: if( - style(--_csstools-p-9fc6ed48-7--alpha: private): cyan; + style(--_csstools-p-8a2d0276-7--alpha: private): cyan; else: orange; ); --baz: if( - style(--something: var(--_csstools-p-9fc6ed48-7--alpha)): cyan; + style(--something: var(--_csstools-p-8a2d0276-7--alpha)): cyan; else: orange; ); } .invalid-private-contents{ - --_csstools-p-9fc6ed48-8--alpha: 2px; + --_csstools-p-8a2d0276-8--alpha: 2px; - --beta: var(--_csstools-p-9fc6ed48-8--alpha); + --beta: var(--_csstools-p-8a2d0276-8--alpha); } .conditional-private { - --_csstools-p-9fc6ed48-9--alpha: 2px; + --_csstools-p-8a2d0276-9--alpha: 2px; @media screen { - --_csstools-p-9fc6ed48-9--alpha: 3px; + --_csstools-p-8a2d0276-9--alpha: 3px; } - --beta: var(--_csstools-p-9fc6ed48-9--alpha); + --beta: var(--_csstools-p-8a2d0276-9--alpha); } .conditional-private-use { - --_csstools-p-9fc6ed48-a--alpha: 2px; + --_csstools-p-8a2d0276-a--alpha: 2px; @media screen { - --beta: var(--_csstools-p-9fc6ed48-a--alpha); + --beta: var(--_csstools-p-8a2d0276-a--alpha); } } @@ -120,19 +120,19 @@ section > h1 > small { } .ignore { - --_csstools-p-9fc6ed48-b--alpha: 2px; + --_csstools-p-8a2d0276-b--alpha: 2px; --beta: var(--foo, --alpha); } .ignore { - --_csstools-p-9fc6ed48-c--alpha: 2px; + --_csstools-p-8a2d0276-c--alpha: 2px; --beta: --alpha; } .ignore { - --_csstools-p-9fc6ed48-d--alpha: 2px; + --_csstools-p-8a2d0276-d--alpha: 2px; --bar: if( style(--foo: --alpha): cyan; @@ -141,7 +141,7 @@ section > h1 > small { } .ignore { - --_csstools-p-9fc6ed48-e--alpha: 2px; + --_csstools-p-8a2d0276-e--alpha: 2px; & .bar { @container style(--something: --alpha) { diff --git a/plugins/postcss-private-rule/test/examples/example.expect.css b/plugins/postcss-private-rule/test/examples/example.expect.css index 95c2525d96..3ad27f70eb 100644 --- a/plugins/postcss-private-rule/test/examples/example.expect.css +++ b/plugins/postcss-private-rule/test/examples/example.expect.css @@ -1,8 +1,8 @@ section { - --_csstools-p-c9ee5fbd-0--size: 2px; + --_csstools-p-15ccaace-0--size: 2px; &, & > h1 { - border-width: var(--_csstools-p-c9ee5fbd-0--size); + border-width: var(--_csstools-p-15ccaace-0--size); } } From 1abda645faf622a96c6548b04e2156dc991fb534 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Sun, 30 Aug 2026 18:32:09 +0200 Subject: [PATCH 5/5] fix --- plugins/postcss-private-rule/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/postcss-private-rule/README.md b/plugins/postcss-private-rule/README.md index 1d7f0f254b..663d63a1f7 100644 --- a/plugins/postcss-private-rule/README.md +++ b/plugins/postcss-private-rule/README.md @@ -26,10 +26,10 @@ section > h1 { /* becomes */ section { - --_csstools-p-c9ee5fbd-0--size: 2px; + --_csstools-p-15ccaace-0--size: 2px; &, & > h1 { - border-width: var(--_csstools-p-c9ee5fbd-0--size); + border-width: var(--_csstools-p-15ccaace-0--size); } }