stylelint-plugin-rhythmguard 3.2.0 → 3.4.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 (38) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CONTRIBUTING.md +19 -3
  3. package/README.md +2 -1
  4. package/package.json +9 -8
  5. package/src/audit/args.js +103 -307
  6. package/src/audit/config.js +1 -1
  7. package/src/audit/contract.js +6 -28
  8. package/src/audit/index.js +4 -7
  9. package/src/audit/render-markdown.js +3 -0
  10. package/src/audit/render-text.js +2 -1
  11. package/src/audit/report.js +26 -19
  12. package/src/audit/scan/files.js +262 -0
  13. package/src/audit/scan/stylesheets.js +275 -0
  14. package/src/audit/scan/templates.js +175 -0
  15. package/src/cli/doctor.js +1 -1
  16. package/src/cli/quickstart.js +5 -2
  17. package/src/{utils → core}/length.js +24 -1
  18. package/src/{utils → core}/options.js +32 -108
  19. package/src/core/scale-inference.js +560 -0
  20. package/src/{utils → core}/token-packages.json +7 -0
  21. package/src/{utils → core}/token-sources.js +155 -17
  22. package/src/{utils/value-utils.js → core/value-nodes.js} +1 -17
  23. package/src/eslint/rules/tailwind-class-use-motion-scale.js +2 -2
  24. package/src/eslint/rules/tailwind-class-use-scale.js +2 -2
  25. package/src/rules/no-offscale-transform/index.js +22 -91
  26. package/src/rules/prefer-token/index.js +15 -73
  27. package/src/rules/report.js +75 -0
  28. package/src/rules/use-motion-scale/index.js +25 -42
  29. package/src/rules/use-scale/index.js +21 -94
  30. package/src/rules/validate.js +132 -0
  31. package/types/audit.d.ts +11 -0
  32. package/src/audit/scan.js +0 -676
  33. package/src/utils/scale-inference.js +0 -351
  34. /package/src/{utils/constants.js → core/css-vocabulary.js} +0 -0
  35. /package/src/{utils → core}/tailwind-class-analysis.js +0 -0
  36. /package/src/{utils → core}/tailwind-motion-analysis.js +0 -0
  37. /package/src/{utils → core}/time.js +0 -0
  38. /package/src/{utils → core}/token-map.js +0 -0
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The Stylelint side of a rule: everything that touches `stylelint.utils` or
5
+ * the PostCSS result lives here, so that src/core stays free of the framework
6
+ * and the same primitives can serve the ESLint plugin and the audit.
7
+ */
8
+ const stylelint = require('stylelint');
9
+ const { declarationValueIndex } = require('../core/value-nodes');
10
+
11
+ /**
12
+ * Compile a user-supplied token pattern. An invalid pattern is reported once
13
+ * against the root and replaced with the historical default so the file still
14
+ * lints instead of throwing inside Stylelint.
15
+ */
16
+ function createTokenRegex(tokenPattern, result, ruleName) {
17
+ try {
18
+ return new RegExp(tokenPattern);
19
+ } catch {
20
+ stylelint.utils.report({
21
+ message: `Invalid tokenPattern regex: ${tokenPattern}`,
22
+ result,
23
+ ruleName,
24
+ });
25
+
26
+ return /^--space-/;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Report one value node inside a declaration, positioned on the node rather
32
+ * than the whole declaration, with an optional fix. The default fix replaces
33
+ * the node's text; pass `fix` for anything else.
34
+ */
35
+ function reportValueNode({ decl, fix = null, length = null, message, node, replacement = null, result, ruleName }) {
36
+ const index = declarationValueIndex(decl) + node.sourceIndex;
37
+ const payload = {
38
+ endIndex: index + (length === null ? node.value.length : length),
39
+ index,
40
+ message,
41
+ node: decl,
42
+ result,
43
+ ruleName,
44
+ };
45
+
46
+ if (fix) {
47
+ payload.fix = fix;
48
+ } else if (replacement) {
49
+ payload.fix = () => {
50
+ node.value = replacement;
51
+ return true;
52
+ };
53
+ }
54
+
55
+ stylelint.utils.report(payload);
56
+ }
57
+
58
+ /** A preset name the options did not recognise is reported once, on the root. */
59
+ function reportInvalidPreset(options, { message, result, root, ruleName }) {
60
+ if (!options.invalidPreset) {
61
+ return;
62
+ }
63
+ stylelint.utils.report({
64
+ message: message(options.invalidPreset, options.presetNames),
65
+ node: root,
66
+ result,
67
+ ruleName,
68
+ });
69
+ }
70
+
71
+ module.exports = {
72
+ createTokenRegex,
73
+ reportInvalidPreset,
74
+ reportValueNode,
75
+ };
@@ -5,11 +5,10 @@ const valueParser = require('postcss-value-parser');
5
5
  const {
6
6
  nearestScaleValues,
7
7
  numbersEqual,
8
- } = require('../../utils/length');
8
+ } = require('../../core/length');
9
9
  const {
10
- declarationValueIndex,
11
10
  walkRootValueNodes,
12
- } = require('../../utils/value-utils');
11
+ } = require('../../core/value-nodes');
13
12
  const {
14
13
  formatTime,
15
14
  fromMs,
@@ -17,7 +16,10 @@ const {
17
16
  normalizeDurationUnits,
18
17
  parseTimeToken,
19
18
  toMs,
20
- } = require('../../utils/time');
19
+ } = require('../../core/time');
20
+
21
+ const { reportValueNode } = require('../report');
22
+ const { validatePrimary } = require('../validate');
21
23
 
22
24
  const ruleName = 'rhythmguard/use-motion-scale';
23
25
  const DURATION_PROPERTIES = new Set([
@@ -99,12 +101,7 @@ function functionToString(node) {
99
101
 
100
102
  const ruleFunction = (primary, secondaryOptions) => {
101
103
  return (root, result) => {
102
- const valid = stylelint.utils.validateOptions(result, ruleName, {
103
- actual: primary,
104
- possible: [true],
105
- });
106
-
107
- if (!valid || !validateSecondaryOptions(result, secondaryOptions)) {
104
+ if (!validatePrimary(result, ruleName, primary) || !validateSecondaryOptions(result, secondaryOptions)) {
108
105
  return;
109
106
  }
110
107
 
@@ -120,10 +117,8 @@ const ruleFunction = (primary, secondaryOptions) => {
120
117
  let changed = false;
121
118
 
122
119
  const reportDuration = (node, nearest, fixedValue = null) => {
123
- const index = declarationValueIndex(decl) + node.sourceIndex;
124
- const payload = {
125
- endIndex: index + node.value.length,
126
- index,
120
+ reportValueNode({
121
+ decl,
127
122
  message: nearest
128
123
  ? messages.rejectedDuration(
129
124
  node.value,
@@ -131,43 +126,31 @@ const ruleFunction = (primary, secondaryOptions) => {
131
126
  formatTime(nearest.upper, 'ms'),
132
127
  )
133
128
  : messages.invalidDuration(node.value),
134
- node: decl,
129
+ node,
130
+ replacement: fixedValue,
135
131
  result,
136
132
  ruleName,
137
- };
138
-
139
- if (fixedValue) {
140
- payload.fix = () => {
141
- node.value = fixedValue;
142
- return true;
143
- };
144
- }
145
-
146
- stylelint.utils.report(payload);
133
+ });
147
134
  };
148
135
 
149
136
  const reportEasing = (node, replacement = null) => {
150
137
  const source = functionToString(node);
151
- const index = declarationValueIndex(decl) + node.sourceIndex;
152
- const payload = {
153
- endIndex: index + source.length,
154
- index,
138
+ reportValueNode({
139
+ decl,
140
+ fix: replacement
141
+ ? () => {
142
+ node.type = 'word';
143
+ node.value = replacement;
144
+ delete node.nodes;
145
+ return true;
146
+ }
147
+ : null,
148
+ length: source.length,
155
149
  message: messages.rejectedEasing(source),
156
- node: decl,
150
+ node,
157
151
  result,
158
152
  ruleName,
159
- };
160
-
161
- if (replacement) {
162
- payload.fix = () => {
163
- node.type = 'word';
164
- node.value = replacement;
165
- delete node.nodes;
166
- return true;
167
- };
168
- }
169
-
170
- stylelint.utils.report(payload);
153
+ });
171
154
  };
172
155
 
173
156
  walkRootValueNodes(parsed, (node) => {
@@ -3,24 +3,19 @@
3
3
  const stylelint = require('stylelint');
4
4
  const valueParser = require('postcss-value-parser');
5
5
  const {
6
+ fixedLengthValue,
6
7
  formatLength,
7
- fromPx,
8
8
  isHairlineLength,
9
9
  nearestScaleValues,
10
- normalizeScale,
11
- normalizeScaleByUnit,
12
10
  numbersEqual,
13
11
  parseLengthToken,
14
12
  toPx,
15
- } = require('../../utils/length');
13
+ } = require('../../core/length');
16
14
  const {
17
15
  buildScaleOptions,
18
- resolvePropertyScale,
19
- validateUseScaleSecondaryOptions,
20
- } = require('../../utils/options');
16
+ createPropertyScaleResolver,
17
+ } = require('../../core/options');
21
18
  const {
22
- createTokenRegex,
23
- declarationValueIndex,
24
19
  isKeyword,
25
20
  isMathFunction,
26
21
  isTokenFunction,
@@ -28,13 +23,15 @@ const {
28
23
  shouldLintMathArgument,
29
24
  walkRootValueNodes,
30
25
  walkTransformTranslateNodes,
31
- } = require('../../utils/value-utils');
26
+ } = require('../../core/value-nodes');
32
27
 
33
28
  const {
34
- DEFAULT_AUTO_TOKEN_PATTERN,
35
29
  autoScaleFallbackNote,
36
- resolveAutoScale,
37
- } = require('../../utils/scale-inference');
30
+ withResolvedScale,
31
+ } = require('../../core/scale-inference');
32
+
33
+ const { createTokenRegex, reportInvalidPreset, reportValueNode } = require('../report');
34
+ const { validatePrimary, validateUseScaleSecondaryOptions } = require('../validate');
38
35
 
39
36
  const ruleName = 'rhythmguard/use-scale';
40
37
 
@@ -45,28 +42,6 @@ const messages = stylelint.utils.ruleMessages(ruleName, {
45
42
  `Unexpected off-scale value "${value}". Use scale values (nearest: ${lower} or ${upper}).${note ? ` ${note}` : ''}`,
46
43
  });
47
44
 
48
- function getFixedNodeValue(parsedLength, nearestPx, options) {
49
- const unit = parsedLength.unit || 'px';
50
-
51
- if (unit === '%' || !options.units.includes(unit)) {
52
- return null;
53
- }
54
-
55
- const signedNearest = parsedLength.number < 0 ? -Math.abs(nearestPx) : nearestPx;
56
-
57
- if (options.unitStrategy === 'exact') {
58
- return formatLength(signedNearest, parsedLength.unit || 'px');
59
- }
60
-
61
- const converted = fromPx(signedNearest, unit, options.baseFontSize);
62
-
63
- if (converted === null) {
64
- return null;
65
- }
66
-
67
- return formatLength(converted, unit);
68
- }
69
-
70
45
  function checkLengthValue({
71
46
  decl,
72
47
  node,
@@ -135,7 +110,7 @@ function checkLengthValue({
135
110
  }
136
111
 
137
112
  const fixedValue = options.fixToScale
138
- ? getFixedNodeValue(parsedLength, nearest.nearest, options)
113
+ ? fixedLengthValue(parsedLength, nearest.nearest, options)
139
114
  : null;
140
115
 
141
116
  report(node.value, decl, node, nearest, fixedValue, unit);
@@ -159,7 +134,7 @@ function checkLengthValue({
159
134
  }
160
135
 
161
136
  const fixedValue = options.fixToScale
162
- ? getFixedNodeValue(parsedLength, nearest.nearest, options)
137
+ ? fixedLengthValue(parsedLength, nearest.nearest, options)
163
138
  : null;
164
139
 
165
140
  report(node.value, decl, node, nearest, fixedValue, 'px');
@@ -168,10 +143,7 @@ function checkLengthValue({
168
143
 
169
144
  const ruleFunction = (primary, secondaryOptions) => {
170
145
  return (root, result) => {
171
- const valid = stylelint.utils.validateOptions(result, ruleName, {
172
- actual: primary,
173
- possible: [true],
174
- });
146
+ const valid = validatePrimary(result, ruleName, primary);
175
147
 
176
148
  if (!valid) {
177
149
  return;
@@ -187,71 +159,26 @@ const ruleFunction = (primary, secondaryOptions) => {
187
159
  }
188
160
 
189
161
  const options = buildScaleOptions(secondaryOptions);
190
- if (options.invalidPreset) {
191
- stylelint.utils.report({
192
- message: messages.invalidPreset(options.invalidPreset, options.presetNames),
193
- node: root,
194
- result,
195
- ruleName,
196
- });
197
- }
162
+ reportInvalidPreset(options, { message: messages.invalidPreset, result, root, ruleName });
198
163
 
199
- if (options.scaleAuto) {
200
- const inference = resolveAutoScale({
201
- baseFontSize: options.baseFontSize,
202
- root,
203
- scaleSources: options.scaleSources,
204
- tailwindConfigPath: options.tailwindConfigPath,
205
- tokenPattern: options.tokenPatternExplicit ? options.tokenPattern : DEFAULT_AUTO_TOKEN_PATTERN,
206
- });
207
- options.scale = inference.scale;
208
- options.scaleInference = inference;
209
- }
164
+ withResolvedScale(options, root);
210
165
 
211
166
  const tokenRegex = createTokenRegex(options.tokenPattern, result, ruleName);
212
- const scaleCache = new Map();
213
167
  let fallbackNote = autoScaleFallbackNote(options.scaleInference);
214
-
215
- const getScaleStateForProperty = (prop) => {
216
- const cached = scaleCache.get(prop);
217
- if (cached) {
218
- return cached;
219
- }
220
-
221
- const selectedScale = resolvePropertyScale(prop, options);
222
- const next = {
223
- scaleByUnit: normalizeScaleByUnit(selectedScale),
224
- scalePx: normalizeScale(selectedScale, options.baseFontSize),
225
- };
226
-
227
- scaleCache.set(prop, next);
228
- return next;
229
- };
168
+ const getScaleStateForProperty = createPropertyScaleResolver(options);
230
169
 
231
170
  const report = (value, decl, node, nearest, fixedValue = null, nearestUnit = 'px') => {
232
- const index = declarationValueIndex(decl) + node.sourceIndex;
233
- const endIndex = index + node.value.length;
234
171
  const lower = nearest ? formatLength(nearest.lower, nearestUnit) : 'n/a';
235
172
  const upper = nearest ? formatLength(nearest.upper, nearestUnit) : 'n/a';
236
-
237
- const payload = {
238
- endIndex,
239
- index,
173
+ reportValueNode({
174
+ decl,
240
175
  message: messages.rejected(value, lower, upper, fallbackNote),
241
- node: decl,
176
+ node,
177
+ replacement: fixedValue,
242
178
  result,
243
179
  ruleName,
244
- };
245
-
246
- if (fixedValue) {
247
- payload.fix = () => {
248
- node.value = fixedValue;
249
- return true;
250
- };
251
- }
252
-
180
+ });
253
181
  fallbackNote = '';
254
- stylelint.utils.report(payload);
255
182
  };
256
183
 
257
184
  root.walkDecls((decl) => {
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Secondary-option validation through Stylelint's `validateOptions`, plus the
5
+ * shape checks it cannot express (arrays and objects). The schemas themselves
6
+ * live in src/core/options.js and know nothing about Stylelint.
7
+ */
8
+ const stylelint = require('stylelint');
9
+ const {
10
+ NO_OFFSCALE_TRANSFORM_POSSIBLE_OPTIONS,
11
+ NO_OFFSCALE_TRANSFORM_VALIDATION_SCHEMA,
12
+ PREFER_TOKEN_POSSIBLE_OPTIONS,
13
+ PREFER_TOKEN_VALIDATION_SCHEMA,
14
+ USE_SCALE_POSSIBLE_OPTIONS,
15
+ USE_SCALE_VALIDATION_SCHEMA,
16
+ isPlainObject,
17
+ } = require('../core/options');
18
+
19
+ function validateSecondaryOptionShapes(result, ruleName, secondaryOptions, schema) {
20
+ if (secondaryOptions === undefined || secondaryOptions === null) {
21
+ return true;
22
+ }
23
+
24
+ if (!isPlainObject(secondaryOptions)) {
25
+ return true;
26
+ }
27
+
28
+ let valid = true;
29
+
30
+ for (const [optionName, descriptor] of Object.entries(schema)) {
31
+ const optionValue = secondaryOptions[optionName];
32
+ if (optionValue === undefined) {
33
+ continue;
34
+ }
35
+
36
+ const literalAllowed = Array.isArray(descriptor.allowLiterals)
37
+ && descriptor.allowLiterals.includes(optionValue);
38
+
39
+ if (descriptor.expectsArray && !Array.isArray(optionValue) && !literalAllowed) {
40
+ valid = false;
41
+ result.warn(
42
+ `Invalid value ${stringifyOptionValue(optionValue)} for option "${optionName}" of rule "${ruleName}"`,
43
+ { stylelintType: 'invalidOption' },
44
+ );
45
+ result.stylelint.stylelintError = true;
46
+ continue;
47
+ }
48
+
49
+ if (descriptor.expectsObject && !isPlainObject(optionValue)) {
50
+ valid = false;
51
+ result.warn(
52
+ `Invalid value ${stringifyOptionValue(optionValue)} for option "${optionName}" of rule "${ruleName}"`,
53
+ { stylelintType: 'invalidOption' },
54
+ );
55
+ result.stylelint.stylelintError = true;
56
+ }
57
+ }
58
+
59
+ return valid;
60
+ }
61
+
62
+ function stringifyOptionValue(value) {
63
+ if (typeof value === 'string') {
64
+ return `"${value}"`;
65
+ }
66
+
67
+ return `"${JSON.stringify(value)}"`;
68
+ }
69
+
70
+ function validateSecondaryOptions({
71
+ result,
72
+ ruleName,
73
+ secondaryOptions,
74
+ schema,
75
+ possibleOptionMap,
76
+ }) {
77
+ const validOptions = stylelint.utils.validateOptions(result, ruleName, {
78
+ actual: secondaryOptions,
79
+ optional: true,
80
+ possible: possibleOptionMap,
81
+ });
82
+ const validShapes = validateSecondaryOptionShapes(
83
+ result,
84
+ ruleName,
85
+ secondaryOptions,
86
+ schema,
87
+ );
88
+
89
+ return validOptions && validShapes;
90
+ }
91
+
92
+ function validateUseScaleSecondaryOptions(result, ruleName, secondaryOptions) {
93
+ return validateSecondaryOptions({
94
+ result,
95
+ ruleName,
96
+ secondaryOptions,
97
+ schema: USE_SCALE_VALIDATION_SCHEMA,
98
+ possibleOptionMap: USE_SCALE_POSSIBLE_OPTIONS,
99
+ });
100
+ }
101
+
102
+ function validateNoOffscaleTransformSecondaryOptions(result, ruleName, secondaryOptions) {
103
+ return validateSecondaryOptions({
104
+ result,
105
+ ruleName,
106
+ secondaryOptions,
107
+ schema: NO_OFFSCALE_TRANSFORM_VALIDATION_SCHEMA,
108
+ possibleOptionMap: NO_OFFSCALE_TRANSFORM_POSSIBLE_OPTIONS,
109
+ });
110
+ }
111
+
112
+ function validatePreferTokenSecondaryOptions(result, ruleName, secondaryOptions) {
113
+ return validateSecondaryOptions({
114
+ result,
115
+ ruleName,
116
+ secondaryOptions,
117
+ schema: PREFER_TOKEN_VALIDATION_SCHEMA,
118
+ possibleOptionMap: PREFER_TOKEN_POSSIBLE_OPTIONS,
119
+ });
120
+ }
121
+
122
+ /** Every Rhythmguard rule takes `true` as its primary option and nothing else. */
123
+ function validatePrimary(result, ruleName, primary) {
124
+ return stylelint.utils.validateOptions(result, ruleName, { actual: primary, possible: [true] });
125
+ }
126
+
127
+ module.exports = {
128
+ validatePrimary,
129
+ validateNoOffscaleTransformSecondaryOptions,
130
+ validatePreferTokenSecondaryOptions,
131
+ validateUseScaleSecondaryOptions,
132
+ };
package/types/audit.d.ts CHANGED
@@ -98,9 +98,19 @@ export interface AuditBaselineComparison {
98
98
 
99
99
  export type AuditScaleSource = "default" | "explicit" | "fallback" | "scanned-css" | "token-package" | "token-sources";
100
100
 
101
+ export interface AuditScaleRejected {
102
+ files?: string[];
103
+ /** Why the inferred set was not accepted as a scale, for example "no common step". */
104
+ reasons: string[];
105
+ source: AuditScaleSource;
106
+ values: Array<number | string>;
107
+ }
108
+
101
109
  export interface AuditScale {
102
110
  /** Files the scale was derived from (token sources or scanned stylesheets). Empty for explicit, default and fallback. */
103
111
  files: string[];
112
+ /** Present when `source` is "fallback" because an inferred scale failed the plausibility check. */
113
+ rejected?: AuditScaleRejected | null;
104
114
  source: AuditScaleSource;
105
115
  tokenCount: number;
106
116
  values: Array<number | string>;
@@ -139,6 +149,7 @@ export interface AuditContractReport {
139
149
  files: string[];
140
150
  offScaleProperties?: Record<string, number>;
141
151
  offScaleValues?: unknown;
152
+ rejected: AuditScaleRejected | null;
142
153
  source: AuditScaleSource;
143
154
  tokenOpportunities?: unknown;
144
155
  values: Array<number | string> | null;