Skip to content

Commit 750278d

Browse files
authored
design-tokens : add support for at rules (#717)
* design-tokens : add support for at rules * lint * more error handling * optimize * Update plugins/postcss-design-tokens/src/transform.ts
1 parent 2002860 commit 750278d

21 files changed

+323
-73
lines changed

package-lock.json

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/postcss-design-tokens/.tape.mjs

+7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ postcssTape(plugin)({
1111
],
1212
warnings: 1
1313
},
14+
'at-rule': {
15+
message: "supports at rules",
16+
},
17+
'at-rule-error': {
18+
message: "supports at rules",
19+
warnings: 1
20+
},
1421
'units': {
1522
message: "supports units usage",
1623
plugins: [

plugins/postcss-design-tokens/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Unreleased (major)
44

55
- Updated: Support for Node v14+ (major).
6+
- Added support for design tokens in at rules (`@media`, `@supports`, ...)
67

78
### 1.2.0 (September 7, 2022)
89

plugins/postcss-design-tokens/README.md

+30-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
},
1414
"size": {
1515
"spacing": {
16-
"small": { "value": "16px" }
16+
"small": { "value": "16px" },
17+
"medium": { "value": "18px" }
1718
}
19+
},
20+
"viewport": {
21+
"medium": { "value": "35rem" }
1822
}
1923
}
2024
```
@@ -29,6 +33,12 @@
2933
padding-bottom: design-token('size.spacing.small' to rem);
3034
}
3135
36+
@media (min-width: design-token('viewport.medium')) {
37+
.foo {
38+
padding-bottom: design-token('size.spacing.medium' to rem);
39+
}
40+
}
41+
3242
/* becomes */
3343
3444
.foo {
@@ -37,6 +47,12 @@
3747
padding-left: 16px;
3848
padding-bottom: 1rem;
3949
}
50+
51+
@media (min-width: 35rem) {
52+
.foo {
53+
padding-bottom: 1.1rem;
54+
}
55+
}
4056
```
4157

4258
## Usage
@@ -195,6 +211,12 @@ postcssDesignTokens({
195211
padding-bottom: design-token('size.spacing.small' to rem);
196212
}
197213
214+
@media (min-width: design-token('viewport.medium')) {
215+
.foo {
216+
padding-bottom: design-token('size.spacing.medium' to rem);
217+
}
218+
}
219+
198220
/* becomes */
199221
200222
.foo {
@@ -203,6 +225,12 @@ postcssDesignTokens({
203225
padding-left: 16px;
204226
padding-bottom: 0.8rem;
205227
}
228+
229+
@media (min-width: 35rem) {
230+
.foo {
231+
padding-bottom: 0.9rem;
232+
}
233+
}
206234
```
207235

208236
### Customize function and at rule names
@@ -290,7 +318,7 @@ The `@design-tokens` rule is used to import design tokens from a JSON file into
290318
@design-tokens url('./tokens-dark-mode.json') format('style-dictionary3') when('dark');
291319
```
292320

293-
You can also import tokens from an `npm` pacakge:
321+
You can also import tokens from an `npm` package:
294322

295323
```pcss
296324
@design-tokens url('node_modules://my-npm-package/tokens.json') format('style-dictionary3');

plugins/postcss-design-tokens/docs/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ The `@design-tokens` rule is used to import design tokens from a JSON file into
187187
@design-tokens url('./tokens-dark-mode.json') format('style-dictionary3') when('dark');
188188
```
189189

190-
You can also import tokens from an `npm` pacakge:
190+
You can also import tokens from an `npm` package:
191191

192192
```pcss
193193
@design-tokens url('node_modules://my-npm-package/tokens.json') format('style-dictionary3');

plugins/postcss-design-tokens/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
"dist"
3939
],
4040
"dependencies": {
41+
"@csstools/css-parser-algorithms": "^1.0.0",
42+
"@csstools/css-tokenizer": "^1.0.0",
4143
"postcss-value-parser": "^4.2.0"
4244
},
4345
"peerDependencies": {

plugins/postcss-design-tokens/src/index.ts

+24-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Token } from './data-formats/base/token';
33
import { tokensFromImport } from './data-formats/parse-import';
44
import { mergeTokens } from './data-formats/token';
55
import { parsePluginOptions, pluginOptions } from './options';
6-
import { onCSSValue } from './values';
6+
import { transform } from './transform';
77

88
const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
99
const options = parsePluginOptions(opts);
@@ -68,12 +68,32 @@ const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
6868
return;
6969
}
7070

71-
const modifiedValue = onCSSValue(tokens, result, decl, options);
72-
if (modifiedValue === decl.value) {
71+
try {
72+
const modifiedValue = transform(tokens, result, decl, decl.value, options);
73+
if (modifiedValue === decl.value) {
74+
return;
75+
}
76+
77+
decl.value = modifiedValue;
78+
} catch (err) {
79+
decl.warn(result, `Failed to parse and transform "${decl.value}"`);
80+
}
81+
},
82+
AtRule(atRule, { result }) {
83+
if (!atRule.params.toLowerCase().includes(options.valueFunctionName)) {
7384
return;
7485
}
7586

76-
decl.value = modifiedValue;
87+
try {
88+
const modifiedValue = transform(tokens, result, atRule, atRule.params, options);
89+
if (modifiedValue === atRule.params) {
90+
return;
91+
}
92+
93+
atRule.params = modifiedValue;
94+
} catch(err) {
95+
atRule.warn(result, `Failed to parse and transform "${atRule.params}"`);
96+
}
7797
},
7898
};
7999
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { parseListOfComponentValues } from '@csstools/css-parser-algorithms';
2+
import { CSSToken, tokenizer } from '@csstools/css-tokenizer';
3+
4+
export function parseComponentValuesFromTokens(tokens: Array<CSSToken>) {
5+
return parseListOfComponentValues(tokens, {
6+
onParseError: (err) => {
7+
throw new Error(JSON.stringify(err));
8+
},
9+
});
10+
}
11+
12+
export function parseComponentValues(source: string) {
13+
const t = tokenizer({ css: source }, {
14+
commentsAreTokens: true,
15+
onParseError: (err) => {
16+
throw new Error(JSON.stringify(err));
17+
},
18+
});
19+
20+
const tokens: Array<CSSToken> = [];
21+
22+
{
23+
while (!t.endOfFile()) {
24+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
25+
tokens.push(t.nextToken()!);
26+
}
27+
28+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
29+
tokens.push(t.nextToken()!); // EOF-token
30+
}
31+
32+
return parseComponentValuesFromTokens(tokens);
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { ComponentValue, isCommentNode, isFunctionNode, isTokenNode, isWhitespaceNode } from '@csstools/css-parser-algorithms';
2+
import { TokenType } from '@csstools/css-tokenizer';
3+
import type { Node, Result } from 'postcss';
4+
import { Token, TokenTransformOptions } from './data-formats/base/token';
5+
import { parsedPluginOptions } from './options';
6+
import { parseComponentValues } from './parse-component-values';
7+
8+
export function transform(tokens: Map<string, Token>, result: Result, postCSSNode: Node, source: string, opts: parsedPluginOptions) {
9+
const componentValues = parseComponentValues(source);
10+
11+
let didChangeSomething = false;
12+
componentValues.forEach((componentValue, index) => {
13+
if (!('walk' in componentValue)) {
14+
return;
15+
}
16+
17+
{
18+
const replacements = transformComponentValue(componentValue, tokens, result, postCSSNode, opts);
19+
if (replacements) {
20+
componentValues.splice(index, 1, ...replacements);
21+
didChangeSomething = true;
22+
return false;
23+
}
24+
}
25+
26+
componentValue.walk((entry, nodeIndex) => {
27+
if (typeof nodeIndex === 'string') {
28+
// Should never happen in FunctionNode
29+
return;
30+
}
31+
32+
const replacements = transformComponentValue(entry.node, tokens, result, postCSSNode, opts);
33+
if (replacements) {
34+
entry.parent.value.splice(nodeIndex, 1, ...replacements);
35+
didChangeSomething = true;
36+
return false;
37+
}
38+
});
39+
});
40+
41+
if (!didChangeSomething) {
42+
return source;
43+
}
44+
45+
return componentValues.map((x) => x.toString()).join('');
46+
}
47+
48+
49+
function transformComponentValue(node: ComponentValue, tokens: Map<string, Token>, result: Result, postCSSNode: Node, opts: parsedPluginOptions) {
50+
if (!isFunctionNode(node)) {
51+
return;
52+
}
53+
54+
if (node.nameTokenValue().toLowerCase() !== opts.valueFunctionName) {
55+
return;
56+
}
57+
58+
let tokenName = '';
59+
let operator = '';
60+
let operatorSubject = '';
61+
62+
for (let i = 0; i < node.value.length; i++) {
63+
const subValue = node.value[i];
64+
if (isWhitespaceNode(subValue) || isCommentNode(subValue)) {
65+
continue;
66+
}
67+
68+
if (
69+
!tokenName &&
70+
isTokenNode(subValue) &&
71+
subValue.value[0] === TokenType.String
72+
) {
73+
tokenName = subValue.value[4].value;
74+
continue;
75+
}
76+
77+
if (
78+
tokenName &&
79+
!operator &&
80+
isTokenNode(subValue) &&
81+
subValue.value[0] === TokenType.Ident &&
82+
subValue.value[4].value.toLowerCase() === 'to'
83+
) {
84+
operator = 'to';
85+
continue;
86+
}
87+
88+
if (
89+
tokenName &&
90+
operator &&
91+
isTokenNode(subValue) &&
92+
subValue.value[0] === TokenType.Ident
93+
) {
94+
operatorSubject = subValue.value[4].value;
95+
continue;
96+
}
97+
98+
break;
99+
}
100+
101+
if (!tokenName) {
102+
postCSSNode.warn(result, 'Expected at least a single string literal for the design-token function.');
103+
return;
104+
}
105+
106+
const replacement = tokens.get(tokenName);
107+
if (!replacement) {
108+
postCSSNode.warn(result, `design-token: "${tokenName}" is not configured.`);
109+
return;
110+
}
111+
112+
if (!operator) {
113+
return parseComponentValues(replacement.cssValue());
114+
}
115+
116+
const transformOptions: TokenTransformOptions = {
117+
pluginOptions: opts.unitsAndValues,
118+
};
119+
120+
if (operator === 'to') {
121+
if (!operatorSubject) {
122+
postCSSNode.warn(result, `Invalid or missing unit in "${node.toString()}"`);
123+
return;
124+
}
125+
126+
transformOptions.toUnit = operatorSubject;
127+
128+
try {
129+
return parseComponentValues(replacement.cssValue(transformOptions));
130+
} catch (err) {
131+
postCSSNode.warn(result, (err as Error).message);
132+
return;
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)