postcss-custom-supports 0.1.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +64 -0
  2. package/index.js +75 -34
  3. package/package.json +12 -4
package/README.md CHANGED
@@ -54,6 +54,8 @@ npm install --save-dev postcss-custom-supports
54
54
 
55
55
  ## Usage
56
56
 
57
+ **CommonJS**
58
+
57
59
  ```js
58
60
  const postcss = require('postcss');
59
61
  const customSupports = require('postcss-custom-supports');
@@ -61,6 +63,66 @@ const customSupports = require('postcss-custom-supports');
61
63
  postcss([customSupports(/* options */)]).process(css);
62
64
  ```
63
65
 
66
+ **Vite** — add to `css.postcss` in `vite.config.js` / `vite.config.ts`:
67
+
68
+ ```js
69
+ import { defineConfig } from 'vite';
70
+ import customSupports from 'postcss-custom-supports';
71
+
72
+ export default defineConfig({
73
+ css: {
74
+ postcss: {
75
+ plugins: [customSupports(/* options */)],
76
+ },
77
+ },
78
+ });
79
+ ```
80
+
81
+ **Tailwind CSS v3** — add to `postcss.config.mjs` alongside your other PostCSS plugins:
82
+
83
+ ```js
84
+ import tailwindcss from 'tailwindcss';
85
+ import customSupports from 'postcss-custom-supports';
86
+
87
+ export default {
88
+ plugins: [
89
+ tailwindcss,
90
+ customSupports(/* options */),
91
+ ],
92
+ };
93
+ ```
94
+
95
+ **Tailwind CSS v4 + Vite** — Tailwind ships as a Vite plugin in v4; add this plugin to `css.postcss` in `vite.config.js`:
96
+
97
+ ```js
98
+ import { defineConfig } from 'vite';
99
+ import tailwindcss from '@tailwindcss/vite';
100
+ import customSupports from 'postcss-custom-supports';
101
+
102
+ export default defineConfig({
103
+ plugins: [tailwindcss()],
104
+ css: {
105
+ postcss: {
106
+ plugins: [customSupports(/* options */)],
107
+ },
108
+ },
109
+ });
110
+ ```
111
+
112
+ **Tailwind CSS v4 + PostCSS** (non-Vite) — use `@tailwindcss/postcss` in `postcss.config.mjs`:
113
+
114
+ ```js
115
+ import tailwindcss from '@tailwindcss/postcss';
116
+ import customSupports from 'postcss-custom-supports';
117
+
118
+ export default {
119
+ plugins: [
120
+ tailwindcss,
121
+ customSupports(/* options */),
122
+ ],
123
+ };
124
+ ```
125
+
64
126
  ### Options
65
127
 
66
128
  | Option | Type | Default | Description |
@@ -87,6 +149,8 @@ A reference is the literal token `(--<name>)`, used anywhere a parenthesized sup
87
149
 
88
150
  References inside function calls (`var(--name)`, `attr(--name)`, etc.) are **not** rewritten, so it is safe to reference custom properties in supports conditions.
89
151
 
152
+ _Note: When we call a custom supports token, we wrap it in parentheses. This follows the structure of postcss-custom-media, but it also gives an easier visual indicator of the token, compared to without parentheses. It’s a taste thing, but it made sense to me._
153
+
90
154
  ## Warnings
91
155
 
92
156
  The plugin emits PostCSS warnings (not errors) for:
package/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const valueParser = require('postcss-value-parser');
4
+
3
5
  // Matches a single @custom-supports declaration:
4
6
  // @custom-supports --name <condition>;
5
7
  // PostCSS strips the trailing semicolon and normalizes whitespace before
@@ -7,47 +9,86 @@
7
9
  // is all we need to require here.
8
10
  const DEFINITION_RE = /^(--[\w-]+)\s+(.+)$/;
9
11
 
10
- // Matches a (--name) reference inside an @supports condition. The negative
11
- // lookbehind prevents accidental rewrites of identifiers like var(--foo) or
12
- // attr(--bar), where the leading character before "(" is an ident-char.
13
- const REFERENCE_RE = /(?<![\w-])\(--[\w-]+\)/g;
14
-
15
12
  const creator = (opts = {}) => {
16
13
  const preserve = opts.preserve === true;
17
14
 
18
15
  return {
19
16
  postcssPlugin: 'postcss-custom-supports',
20
- Once(root, { result }) {
17
+ // prepare() gives us per-process state isolation: a fresh Map per file.
18
+ // Collection runs as a typed AtRule visitor (joins the main walk for free),
19
+ // and replacement defers to OnceExit so all definitions — including ones
20
+ // declared after their first reference — are resolved before rewriting.
21
+ prepare() {
21
22
  const customs = new Map();
22
23
 
23
- root.walkAtRules('custom-supports', rule => {
24
- const match = rule.params.match(DEFINITION_RE);
25
- if (!match) {
26
- rule.warn(result, `Invalid @custom-supports declaration: "${rule.params}"`);
27
- if (!preserve) rule.remove();
28
- return;
29
- }
30
-
31
- const [, name, condition] = match;
32
- if (customs.has(name)) {
33
- rule.warn(result, `@custom-supports ${name} is redefined; the last definition wins`);
34
- }
35
- customs.set(name, condition.trim());
36
-
37
- if (!preserve) rule.remove();
38
- });
39
-
40
- root.walkAtRules('supports', rule => {
41
- rule.params = rule.params.replace(REFERENCE_RE, token => {
42
- const name = token.slice(1, -1);
43
- const value = customs.get(name);
44
- if (value === undefined) {
45
- rule.warn(result, `Unknown @custom-supports reference: ${name}`);
46
- return token;
47
- }
48
- return `(${value})`;
49
- });
50
- });
24
+ return {
25
+ AtRule: {
26
+ 'custom-supports'(rule, { result }) {
27
+ const match = rule.params.match(DEFINITION_RE);
28
+ if (!match) {
29
+ rule.warn(result, `Invalid @custom-supports declaration: "${rule.params}"`);
30
+ if (!preserve) rule.remove();
31
+ return;
32
+ }
33
+
34
+ const [, name, condition] = match;
35
+ if (customs.has(name)) {
36
+ rule.warn(result, `@custom-supports ${name} is redefined; the last definition wins`);
37
+ }
38
+ customs.set(name, condition.trim());
39
+
40
+ if (!preserve) rule.remove();
41
+ },
42
+ },
43
+
44
+ OnceExit(root, { result }) {
45
+ // Recurse bottom-up so that inner @supports rules are rewritten
46
+ // before the outer rule is cloned. If we used root.walkAtRules()
47
+ // instead, replaceWith(clone) would cause the walker to descend into
48
+ // the detached original rather than the clone, leaving inner
49
+ // references unexpanded.
50
+ const processContainer = container => {
51
+ container.each(node => {
52
+ if (node.nodes) processContainer(node);
53
+ if (node.type !== 'atrule' || node.name !== 'supports') return;
54
+
55
+ const parsed = valueParser(node.params);
56
+ let changed = false;
57
+
58
+ parsed.walk(valueNode => {
59
+ // value-parser gives bare parentheses type 'function' with an
60
+ // empty value string, which naturally excludes named functions
61
+ // like var(--x) or attr(--x) — cleaner than the lookbehind
62
+ // regex this replaces.
63
+ if (valueNode.type !== 'function' || valueNode.value !== '') return;
64
+ if (valueNode.nodes.length !== 1 || valueNode.nodes[0].type !== 'word') return;
65
+ const name = valueNode.nodes[0].value;
66
+ if (!/^--[\w-]+$/.test(name)) return;
67
+
68
+ const condition = customs.get(name);
69
+ if (condition === undefined) {
70
+ node.warn(result, `Unknown @custom-supports reference: ${name}`);
71
+ return false;
72
+ }
73
+
74
+ // Expand the condition into this paren node; stop descending
75
+ // so we never re-process the freshly substituted nodes.
76
+ valueNode.nodes = valueParser(condition).nodes;
77
+ changed = true;
78
+ return false;
79
+ });
80
+
81
+ if (changed) {
82
+ // .clone() preserves the original node's source position so
83
+ // PostCSS can emit accurate source maps for the rewritten rule.
84
+ node.replaceWith(node.clone({ params: valueParser.stringify(parsed) }));
85
+ }
86
+ });
87
+ };
88
+
89
+ processContainer(root);
90
+ },
91
+ };
51
92
  },
52
93
  };
53
94
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-custom-supports",
3
- "version": "0.1.3",
3
+ "version": "1.0.0",
4
4
  "description": "PostCSS plugin that mirrors @custom-media for @supports queries, letting you alias verbose or experimental feature-detection conditions behind a custom name.",
5
5
  "author": "Chloe Burroughs <chloe@pfr.wtf>",
6
6
  "repository": {
@@ -16,7 +16,7 @@
16
16
  "index.js"
17
17
  ],
18
18
  "scripts": {
19
- "test": "node --test index.test.js"
19
+ "test": "c8 node --test index.test.js"
20
20
  },
21
21
  "keywords": [
22
22
  "postcss",
@@ -24,13 +24,21 @@
24
24
  "css",
25
25
  "supports",
26
26
  "custom-supports",
27
- "feature-queries"
27
+ "feature-queries",
28
+ "atRules"
28
29
  ],
29
30
  "peerDependencies": {
30
31
  "postcss": "^8.4.0"
31
32
  },
33
+ "devDependencies": {
34
+ "c8": "^11.0.0",
35
+ "postcss": "^8.4.0"
36
+ },
32
37
  "engines": {
33
38
  "node": ">=18.0.0"
34
39
  },
35
- "license": "MIT"
40
+ "license": "MIT",
41
+ "dependencies": {
42
+ "postcss-value-parser": "^4.2.0"
43
+ }
36
44
  }