Skip to content

Commit d51dc91

Browse files
committed
build: convert stylelint rules to typescript
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 695dde6 commit d51dc91

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)