Skip to content

Commit 313e3f3

Browse files
authored
build: convert stylelint rules to typescript (#19047)
Converts the Stylelint rules to TypeScript so they're in line with the rest of the project and to make it easier to work with the PostCSS AST.
1 parent f020403 commit 313e3f3

11 files changed

+636
-438
lines changed

.stylelintrc.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
{
22
"plugins": [
3-
"./tools/stylelint/no-prefixes/index.js",
4-
"./tools/stylelint/no-ampersand-beyond-selector-start.js",
5-
"./tools/stylelint/selector-no-deep.js",
6-
"./tools/stylelint/no-nested-mixin.js",
7-
"./tools/stylelint/no-concrete-rules.js",
8-
"./tools/stylelint/no-top-level-ampersand-in-mixin.js"
3+
"./tools/stylelint/loader-rule.js",
4+
"./tools/stylelint/no-prefixes/index.ts",
5+
"./tools/stylelint/no-ampersand-beyond-selector-start.ts",
6+
"./tools/stylelint/selector-no-deep.ts",
7+
"./tools/stylelint/no-nested-mixin.ts",
8+
"./tools/stylelint/no-concrete-rules.ts",
9+
"./tools/stylelint/no-top-level-ampersand-in-mixin.ts"
910
],
1011
"rules": {
1112
"material/no-prefixes": [true, {

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"@firebase/app-types": "^0.3.2",
8484
"@octokit/rest": "16.28.7",
8585
"@schematics/angular": "^9.0.7",
86+
"@types/autoprefixer": "^9.7.2",
8687
"@types/browser-sync": "^2.26.1",
8788
"@types/fs-extra": "^4.0.3",
8889
"@types/glob": "^5.0.33",
@@ -98,6 +99,7 @@
9899
"@types/run-sequence": "^0.0.29",
99100
"@types/semver": "^6.2.0",
100101
"@types/send": "^0.14.5",
102+
"@types/stylelint": "^9.10.1",
101103
"autoprefixer": "^6.7.6",
102104
"axe-webdriverjs": "^1.1.1",
103105
"browser-sync": "^2.26.7",
@@ -136,6 +138,7 @@
136138
"moment": "^2.18.1",
137139
"node-fetch": "^2.6.0",
138140
"parse5": "^5.0.0",
141+
"postcss": "^7.0.27",
139142
"protractor": "^5.4.3",
140143
"requirejs": "^2.3.6",
141144
"rollup": "~1.25.0",
@@ -150,7 +153,7 @@
150153
"semver": "^6.3.0",
151154
"send": "^0.17.1",
152155
"shelljs": "^0.8.3",
153-
"stylelint": "^13.2.0",
156+
"stylelint": "^13.3.1",
154157
"terser": "^4.3.9",
155158
"ts-api-guardian": "^0.5.0",
156159
"ts-node": "^3.0.4",

tools/stylelint/loader-rule.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const path = require('path');
2+
const stylelint = require('stylelint');
3+
4+
// Custom rule that registers all of the custom rules, written in TypeScript, with ts-node. This is
5+
// necessary, because `stylelint` and IDEs won't execute any rules that aren't in a .js file.
6+
require('ts-node').register({
7+
project: path.join(__dirname, '../gulp/tsconfig.json')
8+
});
9+
10+
// Dummy rule so Stylelint doesn't complain that there aren't rules in the file.
11+
module.exports = stylelint.createPlugin('material/loader', () => {});

tools/stylelint/no-ampersand-beyond-selector-start.js renamed to tools/stylelint/no-ampersand-beyond-selector-start.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,40 @@
1-
const stylelint = require('stylelint');
2-
const path = require('path');
1+
import {createPlugin, utils} from 'stylelint';
2+
import {basename} from 'path';
3+
import {Node} from 'postcss';
4+
35
const isStandardSyntaxRule = require('stylelint/lib/utils/isStandardSyntaxRule');
46
const isStandardSyntaxSelector = require('stylelint/lib/utils/isStandardSyntaxSelector');
57

68
const ruleName = 'material/no-ampersand-beyond-selector-start';
7-
const messages = stylelint.utils.ruleMessages(ruleName, {
9+
const messages = utils.ruleMessages(ruleName, {
810
expected: () => 'Ampersand is only allowed at the beginning of a selector',
911
});
1012

13+
/** Config options for the rule. */
14+
interface RuleOptions {
15+
filePattern: string;
16+
}
17+
1118
/**
1219
* Stylelint rule that doesn't allow for an ampersand to be used anywhere
1320
* except at the start of a selector. Skips private mixins.
1421
*
1522
* Based off the `selector-nested-pattern` Stylelint rule.
1623
* Source: https://github.com/stylelint/stylelint/blob/master/lib/rules/selector-nested-pattern/
1724
*/
18-
const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
25+
const plugin = createPlugin(ruleName, (isEnabled: boolean, _options?) => {
1926
return (root, result) => {
20-
if (!isEnabled) return;
27+
if (!isEnabled) {
28+
return;
29+
}
2130

31+
const options = _options as RuleOptions;
2232
const filePattern = new RegExp(options.filePattern);
23-
const fileName = path.basename(root.source.input.file);
33+
const fileName = basename(root.source!.input.file!);
2434

25-
if (!filePattern.test(fileName)) return;
35+
if (!filePattern.test(fileName)) {
36+
return;
37+
}
2638

2739
root.walkRules(rule => {
2840
if (
@@ -36,7 +48,7 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
3648

3749
// Skip rules inside private mixins.
3850
if (!mixinName || !mixinName.startsWith('_')) {
39-
stylelint.utils.report({
51+
utils.report({
4052
result,
4153
ruleName,
4254
message: messages.expected(),
@@ -48,7 +60,7 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
4860
};
4961

5062
/** Walks up the AST and finds the name of the closest mixin. */
51-
function getClosestMixinName(node) {
63+
function getClosestMixinName(node: Node): string | undefined {
5264
let parent = node.parent;
5365

5466
while (parent) {
@@ -58,9 +70,11 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
5870

5971
parent = parent.parent;
6072
}
73+
74+
return undefined;
6175
}
6276
});
6377

6478
plugin.ruleName = ruleName;
6579
plugin.messages = messages;
66-
module.exports = plugin;
80+
export default plugin;
Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,40 @@
1-
const stylelint = require('stylelint');
2-
const path = require('path');
1+
import {createPlugin, utils} from 'stylelint';
2+
import {basename} from 'path';
3+
34
const ruleName = 'material/no-concrete-rules';
4-
const messages = stylelint.utils.ruleMessages(ruleName, {
5+
const messages = utils.ruleMessages(ruleName, {
56
expected: pattern => `CSS rules must be placed inside a mixin for files matching '${pattern}'.`
67
});
78

9+
/** Config options for the rule. */
10+
interface RuleOptions {
11+
filePattern: string;
12+
}
13+
814
/**
915
* Stylelint plugin that will log a warning for all top-level CSS rules.
1016
* Can be used in theme files to ensure that everything is inside a mixin.
1117
*/
12-
const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
18+
const plugin = createPlugin(ruleName, (isEnabled: boolean, _options) => {
1319
return (root, result) => {
14-
if (!isEnabled) return;
20+
if (!isEnabled) {
21+
return;
22+
}
1523

24+
const options = _options as RuleOptions;
1625
const filePattern = new RegExp(options.filePattern);
17-
const fileName = path.basename(root.source.input.file);
26+
const fileName = basename(root.source!.input.file!);
1827

19-
if (!filePattern.test(fileName)) return;
28+
if (!filePattern.test(fileName) || !root.nodes) {
29+
return;
30+
}
2031

2132
// Go through all the nodes and report a warning for every CSS rule or mixin inclusion.
2233
// We use a regular `forEach`, instead of the PostCSS walker utils, because we only care
2334
// about the top-level nodes.
2435
root.nodes.forEach(node => {
2536
if (node.type === 'rule' || (node.type === 'atrule' && node.name === 'include')) {
26-
stylelint.utils.report({
37+
utils.report({
2738
result,
2839
ruleName,
2940
node,
@@ -36,4 +47,4 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
3647

3748
plugin.ruleName = ruleName;
3849
plugin.messages = messages;
39-
module.exports = plugin;
50+
export default plugin;
Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,34 @@
1-
const stylelint = require('stylelint');
2-
3-
const ruleName = 'material/no-nested-mixin';
4-
const messages = stylelint.utils.ruleMessages(ruleName, {
5-
expected: () => 'Nested mixins are not allowed.',
6-
});
7-
8-
9-
/**
10-
* Stylelint plugin that prevents nesting Sass mixins.
11-
*/
12-
const plugin = stylelint.createPlugin(ruleName, isEnabled => {
13-
return (root, result) => {
14-
if (!isEnabled) return;
15-
16-
root.walkAtRules(rule => {
17-
if (rule.name !== 'mixin') return;
18-
19-
rule.walkAtRules(childRule => {
20-
if (childRule.name !== 'mixin') return;
21-
22-
stylelint.utils.report({
23-
result,
24-
ruleName,
25-
message: messages.expected(),
26-
node: childRule
27-
});
28-
});
29-
});
30-
};
31-
});
32-
33-
plugin.ruleName = ruleName;
34-
plugin.messages = messages;
35-
module.exports = plugin;
1+
import {createPlugin, utils} from 'stylelint';
2+
3+
const ruleName = 'material/no-nested-mixin';
4+
const messages = utils.ruleMessages(ruleName, {
5+
expected: () => 'Nested mixins are not allowed.',
6+
});
7+
8+
/**
9+
* Stylelint plugin that prevents nesting Sass mixins.
10+
*/
11+
const plugin = createPlugin(ruleName, (isEnabled: boolean) => {
12+
return (root, result) => {
13+
if (!isEnabled) { return; }
14+
15+
root.walkAtRules(rule => {
16+
if (rule.name !== 'mixin') { return; }
17+
18+
rule.walkAtRules(childRule => {
19+
if (childRule.name !== 'mixin') { return; }
20+
21+
utils.report({
22+
result,
23+
ruleName,
24+
message: messages.expected(),
25+
node: childRule
26+
});
27+
});
28+
});
29+
};
30+
});
31+
32+
plugin.ruleName = ruleName;
33+
plugin.messages = messages;
34+
export default plugin;

tools/stylelint/no-prefixes/index.js renamed to tools/stylelint/no-prefixes/index.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,36 @@
1-
const stylelint = require('stylelint');
2-
const NeedsPrefix = require('./needs-prefix');
3-
const parseSelector = require('stylelint/lib/utils/parseSelector');
4-
const minimatch = require('minimatch');
1+
import {createPlugin, utils} from 'stylelint';
2+
import * as minimatch from 'minimatch';
3+
import {NeedsPrefix} from './needs-prefix';
54

5+
const parseSelector = require('stylelint/lib/utils/parseSelector');
66
const ruleName = 'material/no-prefixes';
7-
const messages = stylelint.utils.ruleMessages(ruleName, {
7+
const messages = utils.ruleMessages(ruleName, {
88
property: property => `Unprefixed property "${property}".`,
99
value: (property, value) => `Unprefixed value in "${property}: ${value}".`,
1010
atRule: name => `Unprefixed @rule "${name}".`,
1111
mediaFeature: value => `Unprefixed media feature "${value}".`,
1212
selector: selector => `Unprefixed selector "${selector}".`
1313
});
1414

15+
/** Config options for the rule. */
16+
interface RuleOptions {
17+
browsers: string[];
18+
filePattern: string;
19+
}
20+
1521
/**
1622
* Stylelint plugin that warns for unprefixed CSS.
1723
*/
18-
const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
24+
const plugin = createPlugin(ruleName, (isEnabled: boolean, _options?) => {
1925
return (root, result) => {
20-
if (!isEnabled || !stylelint.utils.validateOptions(result, ruleName, {})) {
26+
if (!isEnabled) {
2127
return;
2228
}
2329

30+
const options = _options as RuleOptions;
2431
const {browsers, filePattern} = options;
2532

26-
if (filePattern && !minimatch(root.source.input.file, filePattern)) {
33+
if (filePattern && !minimatch(root.source!.input.file!, filePattern)) {
2734
return;
2835
}
2936

@@ -32,15 +39,15 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
3239
// Check all of the `property: value` pairs.
3340
root.walkDecls(decl => {
3441
if (needsPrefix.property(decl.prop)) {
35-
stylelint.utils.report({
42+
utils.report({
3643
result,
3744
ruleName,
3845
message: messages.property(decl.prop),
3946
node: decl,
4047
index: (decl.raws.before || '').length
4148
});
4249
} else if (needsPrefix.value(decl.prop, decl.value)) {
43-
stylelint.utils.report({
50+
utils.report({
4451
result,
4552
ruleName,
4653
message: messages.value(decl.prop, decl.value),
@@ -53,14 +60,14 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
5360
// Check all of the @-rules and their values.
5461
root.walkAtRules(rule => {
5562
if (needsPrefix.atRule(rule.name)) {
56-
stylelint.utils.report({
63+
utils.report({
5764
result,
5865
ruleName,
5966
message: messages.atRule(rule.name),
6067
node: rule
6168
});
6269
} else if (needsPrefix.mediaFeature(rule.params)) {
63-
stylelint.utils.report({
70+
utils.report({
6471
result,
6572
ruleName,
6673
message: messages.mediaFeature(rule.name),
@@ -73,10 +80,10 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
7380
root.walkRules(rule => {
7481
// Silence warnings for Sass selectors. Stylelint does this in their own rules as well:
7582
// https://github.com/stylelint/stylelint/blob/master/lib/utils/isStandardSyntaxSelector.js
76-
parseSelector(rule.selector, { warn: () => {} }, rule, selectorTree => {
77-
selectorTree.walkPseudos(pseudoNode => {
83+
parseSelector(rule.selector, { warn: () => {} }, rule, (selectorTree: any) => {
84+
selectorTree.walkPseudos((pseudoNode: any) => {
7885
if (needsPrefix.selector(pseudoNode.value)) {
79-
stylelint.utils.report({
86+
utils.report({
8087
result,
8188
ruleName,
8289
message: messages.selector(pseudoNode.value),
@@ -94,4 +101,4 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
94101

95102
plugin.ruleName = ruleName;
96103
plugin.messages = messages;
97-
module.exports = plugin;
104+
export default plugin;

0 commit comments

Comments
 (0)