@servicetitan/stylelint-config 40.1.0 → 40.2.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.
- package/dist/function-url-no-absolute.d.ts.map +1 -1
- package/dist/function-url-no-absolute.js +1 -7
- package/dist/function-url-no-absolute.js.map +1 -1
- package/dist/use-tokens/index.d.ts +2 -7
- package/dist/use-tokens/index.d.ts.map +1 -1
- package/dist/use-tokens/index.js +2 -114
- package/dist/use-tokens/index.js.map +1 -1
- package/dist/use-tokens/load-tokens.d.ts +11 -0
- package/dist/use-tokens/load-tokens.d.ts.map +1 -0
- package/dist/use-tokens/load-tokens.js +97 -0
- package/dist/use-tokens/load-tokens.js.map +1 -0
- package/dist/use-tokens/use-tokens.d.ts +9 -0
- package/dist/use-tokens/use-tokens.d.ts.map +1 -0
- package/dist/use-tokens/use-tokens.js +169 -0
- package/dist/use-tokens/use-tokens.js.map +1 -0
- package/dist/use-tokens/values.d.ts +11 -9
- package/dist/use-tokens/values.d.ts.map +1 -1
- package/dist/use-tokens/values.js +59 -22
- package/dist/use-tokens/values.js.map +1 -1
- package/package.json +12 -4
- package/src/__tests__/function-url-no-absolute.test.ts +84 -0
- package/src/function-url-no-absolute.ts +1 -2
- package/src/use-tokens/__tests__/load-tokens.test.ts +135 -0
- package/src/use-tokens/__tests__/use-tokens.test.ts +414 -0
- package/src/use-tokens/index.ts +2 -162
- package/src/use-tokens/load-tokens.ts +103 -0
- package/src/use-tokens/use-tokens.ts +214 -0
- package/src/use-tokens/values.ts +55 -37
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import Color from 'color';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { Declaration } from 'postcss';
|
|
4
|
+
import stylelint, { Rule } from 'stylelint';
|
|
5
|
+
import { DESIGN_SYSTEM_SOURCE, Token, TokenSource } from './load-tokens';
|
|
6
|
+
import {
|
|
7
|
+
borderRadiuses,
|
|
8
|
+
colors,
|
|
9
|
+
fontFamilies,
|
|
10
|
+
fontWeights,
|
|
11
|
+
lineHeights,
|
|
12
|
+
opacities,
|
|
13
|
+
spacings,
|
|
14
|
+
TokenCollection,
|
|
15
|
+
typescales,
|
|
16
|
+
zIndexes,
|
|
17
|
+
} from './values';
|
|
18
|
+
|
|
19
|
+
const { createPlugin, utils } = stylelint;
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* Matches spaces outside color functions, so splitting a value on it keeps a color such as
|
|
23
|
+
* rgb(2, 101, 220) whole while still separating the colors inside a gradient.
|
|
24
|
+
*/
|
|
25
|
+
const spaceOutsideColorFunction = / (?<!\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\([^()]*)/;
|
|
26
|
+
|
|
27
|
+
const ruleName = '@servicetitan/use-tokens';
|
|
28
|
+
const messages = utils.ruleMessages(ruleName, {
|
|
29
|
+
variableExpected: (expected: string, found: string) =>
|
|
30
|
+
`You should use ${expected} instead of "${found}".`,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
interface Linter {
|
|
34
|
+
collection: TokenCollection;
|
|
35
|
+
/* Limits the check to matching properties. Without it, every property is checked. */
|
|
36
|
+
condition?: (property: string) => boolean;
|
|
37
|
+
/* Normalizes a value before it's looked up in the collection. */
|
|
38
|
+
transformer?: (value: string) => string;
|
|
39
|
+
/* Looks up each space-separated part of the value. Without it, the whole value is looked up. */
|
|
40
|
+
multipleValues?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const linters: Linter[] = [
|
|
44
|
+
{
|
|
45
|
+
collection: colors,
|
|
46
|
+
transformer: value => {
|
|
47
|
+
try {
|
|
48
|
+
/* Includes alpha, so a translucent color doesn't match an opaque token. */
|
|
49
|
+
return new Color(value).hexa();
|
|
50
|
+
} catch {
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
multipleValues: true,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
collection: typescales,
|
|
58
|
+
condition: property => property === 'font-size',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
collection: borderRadiuses,
|
|
62
|
+
condition: property => property.startsWith('border-') && property.endsWith('-radius'),
|
|
63
|
+
transformer: value => (value === '0px' ? '0' : value),
|
|
64
|
+
multipleValues: true,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
collection: spacings,
|
|
68
|
+
condition: property => property.startsWith('margin') || property.startsWith('padding'),
|
|
69
|
+
transformer: value => {
|
|
70
|
+
if (value === '0px') {
|
|
71
|
+
return '0';
|
|
72
|
+
}
|
|
73
|
+
return value.startsWith('-') ? value.slice(1) : value;
|
|
74
|
+
},
|
|
75
|
+
multipleValues: true,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
collection: fontWeights,
|
|
79
|
+
condition: property => property === 'font-weight',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
collection: opacities,
|
|
83
|
+
condition: property => property === 'opacity',
|
|
84
|
+
transformer: value => (value.startsWith('0.') ? value.slice(1) : value),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
collection: zIndexes,
|
|
88
|
+
condition: property => property === 'z-index',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
collection: lineHeights,
|
|
92
|
+
condition: property => property === 'line-height',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
collection: fontFamilies,
|
|
96
|
+
condition: property => property === 'font-family',
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const plugin: Rule = (_0, _1, context) => {
|
|
101
|
+
return (root, result) => {
|
|
102
|
+
const extension = path
|
|
103
|
+
.extname(result.opts.from ?? '')
|
|
104
|
+
.slice(1)
|
|
105
|
+
.toLowerCase();
|
|
106
|
+
|
|
107
|
+
/* Returns whether any part of the declaration's value matched a token. */
|
|
108
|
+
function lint(
|
|
109
|
+
decl: Declaration,
|
|
110
|
+
{ collection, condition, transformer, multipleValues }: Linter
|
|
111
|
+
) {
|
|
112
|
+
if (condition && !condition(decl.prop)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const values = multipleValues
|
|
117
|
+
? decl.value.split(spaceOutsideColorFunction)
|
|
118
|
+
: [decl.value];
|
|
119
|
+
|
|
120
|
+
let matched = false;
|
|
121
|
+
for (const rawValue of values) {
|
|
122
|
+
const value = transformer?.(rawValue) ?? rawValue;
|
|
123
|
+
const tokens = collection.get(value);
|
|
124
|
+
if (!tokens) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
matched = true;
|
|
129
|
+
const replacements = getReplacements(tokens, rawValue, extension);
|
|
130
|
+
|
|
131
|
+
if (context.fix && replacements.length === 1) {
|
|
132
|
+
values[values.indexOf(rawValue)] = replacements[0].variable;
|
|
133
|
+
} else {
|
|
134
|
+
/* When several tokens match, the developer picks one. */
|
|
135
|
+
const message = formatMessage(replacements, rawValue);
|
|
136
|
+
utils.report({ node: decl, message, word: rawValue, ruleName, result });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (context.fix) {
|
|
141
|
+
decl.value = values.join(' ');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return matched;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
root.walkDecls(decl => {
|
|
148
|
+
linters.some(linter => lint(decl, linter));
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
plugin.ruleName = ruleName;
|
|
154
|
+
plugin.messages = messages;
|
|
155
|
+
|
|
156
|
+
export = Object.assign(createPlugin(ruleName, plugin), { ruleName, messages });
|
|
157
|
+
|
|
158
|
+
interface Replacement {
|
|
159
|
+
variable: string;
|
|
160
|
+
source: TokenSource;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/*
|
|
164
|
+
* Returns the variables that can replace a literal, negated when the literal is negative, for
|
|
165
|
+
* example "-16px" becomes "-$spacing-2" in Sass.
|
|
166
|
+
*/
|
|
167
|
+
function getReplacements(tokens: Token[], rawValue: string, extension: string) {
|
|
168
|
+
const negative = rawValue.startsWith('-');
|
|
169
|
+
|
|
170
|
+
return tokens.reduce<Replacement[]>((replacements, token) => {
|
|
171
|
+
const plain = convertToVariable(token, extension);
|
|
172
|
+
const variable = negative ? negate(plain, token.source) : plain;
|
|
173
|
+
/*
|
|
174
|
+
* When tokens from different sources match a literal with the same
|
|
175
|
+
* replacement, issue just one replacement that --fix can apply.
|
|
176
|
+
*/
|
|
177
|
+
return replacements.some(replacement => replacement.variable === variable)
|
|
178
|
+
? replacements
|
|
179
|
+
: [...replacements, { variable, source: token.source }];
|
|
180
|
+
}, []);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function convertToVariable({ name, customProperty, source }: Token, extension: string) {
|
|
184
|
+
switch (extension) {
|
|
185
|
+
case 'less':
|
|
186
|
+
/* Anvil2 publishes no Less variables. */
|
|
187
|
+
return source === DESIGN_SYSTEM_SOURCE ? `@${name}` : `var(${customProperty})`;
|
|
188
|
+
|
|
189
|
+
case 'sass':
|
|
190
|
+
case 'scss':
|
|
191
|
+
return `$${name}`;
|
|
192
|
+
|
|
193
|
+
default:
|
|
194
|
+
return `var(${customProperty})`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/*
|
|
199
|
+
* A minus sign negates only a variable that holds a plain number, such as a design-system Sass or
|
|
200
|
+
* Less variable. CSS has no unary minus for var(), and Anvil2's Sass variables can hold var(), so
|
|
201
|
+
* those are negated with calc().
|
|
202
|
+
*/
|
|
203
|
+
function negate(variable: string, source: TokenSource) {
|
|
204
|
+
return source === DESIGN_SYSTEM_SOURCE && !variable.startsWith('var(')
|
|
205
|
+
? `-${variable}`
|
|
206
|
+
: `calc(-1 * ${variable})`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function formatMessage(replacements: Replacement[], found: string) {
|
|
210
|
+
const choices = replacements
|
|
211
|
+
.map(({ variable, source }) => `"${variable}" from \`${source}\``)
|
|
212
|
+
.join(' or ');
|
|
213
|
+
return messages.variableExpected(choices, found);
|
|
214
|
+
}
|
package/src/use-tokens/values.ts
CHANGED
|
@@ -1,46 +1,64 @@
|
|
|
1
|
-
import { tokens } from '@servicetitan/tokens/core';
|
|
2
1
|
import Color from 'color';
|
|
3
|
-
import
|
|
2
|
+
import { ANVIL2_SOURCE, DESIGN_SYSTEM_SOURCE, Token, TokenSource, loadTokens } from './load-tokens';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
str.replace(/([A-Z]|[a-z][0-9])/g, ([letter, digit]) =>
|
|
8
|
-
digit === undefined ? `-${letter.toLowerCase()}` : `${letter}-${digit}`
|
|
9
|
-
);
|
|
4
|
+
/* Maps a literal value to the tokens that hold it, at most one per source. */
|
|
5
|
+
export type TokenCollection = Map<string, Token[]>;
|
|
10
6
|
|
|
11
|
-
const
|
|
12
|
-
.readFileSync(require.resolve('@servicetitan/tokens/core/tokens.less'), 'utf8')
|
|
13
|
-
.split('\n')
|
|
14
|
-
.filter(row => row.startsWith('@') && row.endsWith('(alias)'))
|
|
15
|
-
.map(row => toCamelCase(row.split(':')[0].slice(1)));
|
|
16
|
-
|
|
17
|
-
function getValues(
|
|
18
|
-
condition: (variable: string) => boolean,
|
|
19
|
-
transformer?: (value: string) => string
|
|
20
|
-
) {
|
|
21
|
-
return Object.entries(tokens)
|
|
22
|
-
.filter(([variable]) => !aliases.includes(variable) && condition(variable))
|
|
23
|
-
.reduce<Record<string, string>>(
|
|
24
|
-
(result, [variable, value]) => ({
|
|
25
|
-
...result,
|
|
26
|
-
[transformer?.(value) ?? value]: toSnakeCase(variable),
|
|
27
|
-
}),
|
|
28
|
-
{}
|
|
29
|
-
);
|
|
30
|
-
}
|
|
7
|
+
const tokens = loadTokens();
|
|
31
8
|
|
|
32
9
|
export const colors = getValues(
|
|
33
|
-
|
|
34
|
-
value => new Color(value).
|
|
10
|
+
{ [DESIGN_SYSTEM_SOURCE]: /^color/, [ANVIL2_SOURCE]: /^color-/ },
|
|
11
|
+
value => new Color(value).hexa()
|
|
35
12
|
);
|
|
36
|
-
export const typescales = getValues(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
export const
|
|
41
|
-
|
|
42
|
-
|
|
13
|
+
export const typescales = getValues({
|
|
14
|
+
[DESIGN_SYSTEM_SOURCE]: /^typescale/,
|
|
15
|
+
[ANVIL2_SOURCE]: /^font-size-/,
|
|
16
|
+
});
|
|
17
|
+
export const borderRadiuses = getValues({
|
|
18
|
+
[DESIGN_SYSTEM_SOURCE]: /^border-radius/,
|
|
19
|
+
[ANVIL2_SOURCE]: /^radius-/,
|
|
20
|
+
});
|
|
21
|
+
export const spacings = getValues({
|
|
22
|
+
[DESIGN_SYSTEM_SOURCE]: /^spacing/,
|
|
23
|
+
[ANVIL2_SOURCE]: /^size-/,
|
|
24
|
+
});
|
|
25
|
+
export const fontWeights = getValues({
|
|
26
|
+
[DESIGN_SYSTEM_SOURCE]: /^font-weight/,
|
|
27
|
+
[ANVIL2_SOURCE]: /^font-weight-/,
|
|
28
|
+
});
|
|
29
|
+
export const opacities = getValues({ [DESIGN_SYSTEM_SOURCE]: /^opacity/ });
|
|
30
|
+
export const zIndexes = getValues({ [DESIGN_SYSTEM_SOURCE]: /^z-index/ });
|
|
31
|
+
export const lineHeights = getValues({
|
|
32
|
+
[DESIGN_SYSTEM_SOURCE]: /-line-height$/,
|
|
33
|
+
[ANVIL2_SOURCE]: /^font-line-height-/,
|
|
34
|
+
});
|
|
43
35
|
export const fontFamilies = getValues(
|
|
44
|
-
|
|
36
|
+
{ [DESIGN_SYSTEM_SOURCE]: /-font-family$/, [ANVIL2_SOURCE]: /^font-family-/ },
|
|
45
37
|
value => value.replace(/'/g, "'")
|
|
46
38
|
);
|
|
39
|
+
|
|
40
|
+
function getValues(
|
|
41
|
+
patterns: Partial<Record<TokenSource, RegExp>>,
|
|
42
|
+
transformer?: (value: string) => string
|
|
43
|
+
) {
|
|
44
|
+
return tokens.reduce<TokenCollection>((result, token) => {
|
|
45
|
+
if (!patterns[token.source]?.test(token.name)) {
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* A token the transformer can't normalize, such as an oklch() color, can never match a
|
|
51
|
+
* literal, so it's skipped rather than failing every lint run.
|
|
52
|
+
*/
|
|
53
|
+
let value: string;
|
|
54
|
+
try {
|
|
55
|
+
value = transformer?.(token.value) ?? token.value;
|
|
56
|
+
} catch {
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Within a source, the last token with a given value wins. */
|
|
61
|
+
const matches = (result.get(value) ?? []).filter(({ source }) => source !== token.source);
|
|
62
|
+
return result.set(value, [...matches, token]);
|
|
63
|
+
}, new Map());
|
|
64
|
+
}
|