Skip to content

build: convert stylelint rules to typescript #19047

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .stylelintrc.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"plugins": [
"./tools/stylelint/no-prefixes/index.js",
"./tools/stylelint/no-ampersand-beyond-selector-start.js",
"./tools/stylelint/selector-no-deep.js",
"./tools/stylelint/no-nested-mixin.js",
"./tools/stylelint/no-concrete-rules.js",
"./tools/stylelint/no-top-level-ampersand-in-mixin.js"
"./tools/stylelint/loader-rule.js",
"./tools/stylelint/no-prefixes/index.ts",
"./tools/stylelint/no-ampersand-beyond-selector-start.ts",
"./tools/stylelint/selector-no-deep.ts",
"./tools/stylelint/no-nested-mixin.ts",
"./tools/stylelint/no-concrete-rules.ts",
"./tools/stylelint/no-top-level-ampersand-in-mixin.ts"
],
"rules": {
"material/no-prefixes": [true, {
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"@firebase/app-types": "^0.3.2",
"@octokit/rest": "16.28.7",
"@schematics/angular": "^9.0.7",
"@types/autoprefixer": "^9.7.2",
"@types/browser-sync": "^2.26.1",
"@types/fs-extra": "^4.0.3",
"@types/glob": "^5.0.33",
Expand All @@ -98,6 +99,7 @@
"@types/run-sequence": "^0.0.29",
"@types/semver": "^6.2.0",
"@types/send": "^0.14.5",
"@types/stylelint": "^9.10.1",
"autoprefixer": "^6.7.6",
"axe-webdriverjs": "^1.1.1",
"browser-sync": "^2.26.7",
Expand Down Expand Up @@ -136,6 +138,7 @@
"moment": "^2.18.1",
"node-fetch": "^2.6.0",
"parse5": "^5.0.0",
"postcss": "^7.0.27",
"protractor": "^5.4.3",
"requirejs": "^2.3.6",
"rollup": "~1.25.0",
Expand All @@ -150,7 +153,7 @@
"semver": "^6.3.0",
"send": "^0.17.1",
"shelljs": "^0.8.3",
"stylelint": "^13.2.0",
"stylelint": "^13.3.1",
"terser": "^4.3.9",
"ts-api-guardian": "^0.5.0",
"ts-node": "^3.0.4",
Expand Down
11 changes: 11 additions & 0 deletions tools/stylelint/loader-rule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const path = require('path');
const stylelint = require('stylelint');

// Custom rule that registers all of the custom rules, written in TypeScript, with ts-node. This is
// necessary, because `stylelint` and IDEs won't execute any rules that aren't in a .js file.
require('ts-node').register({
project: path.join(__dirname, '../gulp/tsconfig.json')
});

// Dummy rule so Stylelint doesn't complain that there aren't rules in the file.
module.exports = stylelint.createPlugin('material/loader', () => {});
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
const stylelint = require('stylelint');
const path = require('path');
import {createPlugin, utils} from 'stylelint';
import {basename} from 'path';
import {Node} from 'postcss';

const isStandardSyntaxRule = require('stylelint/lib/utils/isStandardSyntaxRule');
const isStandardSyntaxSelector = require('stylelint/lib/utils/isStandardSyntaxSelector');

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

/** Config options for the rule. */
interface RuleOptions {
filePattern: string;
}

/**
* Stylelint rule that doesn't allow for an ampersand to be used anywhere
* except at the start of a selector. Skips private mixins.
*
* Based off the `selector-nested-pattern` Stylelint rule.
* Source: https://github.com/stylelint/stylelint/blob/master/lib/rules/selector-nested-pattern/
*/
const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
const plugin = createPlugin(ruleName, (isEnabled: boolean, _options?) => {
return (root, result) => {
if (!isEnabled) return;
if (!isEnabled) {
return;
}

const options = _options as RuleOptions;
const filePattern = new RegExp(options.filePattern);
const fileName = path.basename(root.source.input.file);
const fileName = basename(root.source!.input.file!);

if (!filePattern.test(fileName)) return;
if (!filePattern.test(fileName)) {
return;
}

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

// Skip rules inside private mixins.
if (!mixinName || !mixinName.startsWith('_')) {
stylelint.utils.report({
utils.report({
result,
ruleName,
message: messages.expected(),
Expand All @@ -48,7 +60,7 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
};

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

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

parent = parent.parent;
}

return undefined;
}
});

plugin.ruleName = ruleName;
plugin.messages = messages;
module.exports = plugin;
export default plugin;
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
const stylelint = require('stylelint');
const path = require('path');
import {createPlugin, utils} from 'stylelint';
import {basename} from 'path';

const ruleName = 'material/no-concrete-rules';
const messages = stylelint.utils.ruleMessages(ruleName, {
const messages = utils.ruleMessages(ruleName, {
expected: pattern => `CSS rules must be placed inside a mixin for files matching '${pattern}'.`
});

/** Config options for the rule. */
interface RuleOptions {
filePattern: string;
}

/**
* Stylelint plugin that will log a warning for all top-level CSS rules.
* Can be used in theme files to ensure that everything is inside a mixin.
*/
const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
const plugin = createPlugin(ruleName, (isEnabled: boolean, _options) => {
return (root, result) => {
if (!isEnabled) return;
if (!isEnabled) {
return;
}

const options = _options as RuleOptions;
const filePattern = new RegExp(options.filePattern);
const fileName = path.basename(root.source.input.file);
const fileName = basename(root.source!.input.file!);

if (!filePattern.test(fileName)) return;
if (!filePattern.test(fileName) || !root.nodes) {
return;
}

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

plugin.ruleName = ruleName;
plugin.messages = messages;
module.exports = plugin;
export default plugin;
Original file line number Diff line number Diff line change
@@ -1,35 +1,34 @@
const stylelint = require('stylelint');

const ruleName = 'material/no-nested-mixin';
const messages = stylelint.utils.ruleMessages(ruleName, {
expected: () => 'Nested mixins are not allowed.',
});


/**
* Stylelint plugin that prevents nesting Sass mixins.
*/
const plugin = stylelint.createPlugin(ruleName, isEnabled => {
return (root, result) => {
if (!isEnabled) return;

root.walkAtRules(rule => {
if (rule.name !== 'mixin') return;

rule.walkAtRules(childRule => {
if (childRule.name !== 'mixin') return;

stylelint.utils.report({
result,
ruleName,
message: messages.expected(),
node: childRule
});
});
});
};
});

plugin.ruleName = ruleName;
plugin.messages = messages;
module.exports = plugin;
import {createPlugin, utils} from 'stylelint';

const ruleName = 'material/no-nested-mixin';
const messages = utils.ruleMessages(ruleName, {
expected: () => 'Nested mixins are not allowed.',
});

/**
* Stylelint plugin that prevents nesting Sass mixins.
*/
const plugin = createPlugin(ruleName, (isEnabled: boolean) => {
return (root, result) => {
if (!isEnabled) { return; }

root.walkAtRules(rule => {
if (rule.name !== 'mixin') { return; }

rule.walkAtRules(childRule => {
if (childRule.name !== 'mixin') { return; }

utils.report({
result,
ruleName,
message: messages.expected(),
node: childRule
});
});
});
};
});

plugin.ruleName = ruleName;
plugin.messages = messages;
export default plugin;
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
const stylelint = require('stylelint');
const NeedsPrefix = require('./needs-prefix');
const parseSelector = require('stylelint/lib/utils/parseSelector');
const minimatch = require('minimatch');
import {createPlugin, utils} from 'stylelint';
import * as minimatch from 'minimatch';
import {NeedsPrefix} from './needs-prefix';

const parseSelector = require('stylelint/lib/utils/parseSelector');
const ruleName = 'material/no-prefixes';
const messages = stylelint.utils.ruleMessages(ruleName, {
const messages = utils.ruleMessages(ruleName, {
property: property => `Unprefixed property "${property}".`,
value: (property, value) => `Unprefixed value in "${property}: ${value}".`,
atRule: name => `Unprefixed @rule "${name}".`,
mediaFeature: value => `Unprefixed media feature "${value}".`,
selector: selector => `Unprefixed selector "${selector}".`
});

/** Config options for the rule. */
interface RuleOptions {
browsers: string[];
filePattern: string;
}

/**
* Stylelint plugin that warns for unprefixed CSS.
*/
const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
const plugin = createPlugin(ruleName, (isEnabled: boolean, _options?) => {
return (root, result) => {
if (!isEnabled || !stylelint.utils.validateOptions(result, ruleName, {})) {
if (!isEnabled) {
return;
}

const options = _options as RuleOptions;
const {browsers, filePattern} = options;

if (filePattern && !minimatch(root.source.input.file, filePattern)) {
if (filePattern && !minimatch(root.source!.input.file!, filePattern)) {
return;
}

Expand All @@ -32,15 +39,15 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
// Check all of the `property: value` pairs.
root.walkDecls(decl => {
if (needsPrefix.property(decl.prop)) {
stylelint.utils.report({
utils.report({
result,
ruleName,
message: messages.property(decl.prop),
node: decl,
index: (decl.raws.before || '').length
});
} else if (needsPrefix.value(decl.prop, decl.value)) {
stylelint.utils.report({
utils.report({
result,
ruleName,
message: messages.value(decl.prop, decl.value),
Expand All @@ -53,14 +60,14 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
// Check all of the @-rules and their values.
root.walkAtRules(rule => {
if (needsPrefix.atRule(rule.name)) {
stylelint.utils.report({
utils.report({
result,
ruleName,
message: messages.atRule(rule.name),
node: rule
});
} else if (needsPrefix.mediaFeature(rule.params)) {
stylelint.utils.report({
utils.report({
result,
ruleName,
message: messages.mediaFeature(rule.name),
Expand All @@ -73,10 +80,10 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
root.walkRules(rule => {
// Silence warnings for Sass selectors. Stylelint does this in their own rules as well:
// https://github.com/stylelint/stylelint/blob/master/lib/utils/isStandardSyntaxSelector.js
parseSelector(rule.selector, { warn: () => {} }, rule, selectorTree => {
selectorTree.walkPseudos(pseudoNode => {
parseSelector(rule.selector, { warn: () => {} }, rule, (selectorTree: any) => {
selectorTree.walkPseudos((pseudoNode: any) => {
if (needsPrefix.selector(pseudoNode.value)) {
stylelint.utils.report({
utils.report({
result,
ruleName,
message: messages.selector(pseudoNode.value),
Expand All @@ -94,4 +101,4 @@ const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {

plugin.ruleName = ruleName;
plugin.messages = messages;
module.exports = plugin;
export default plugin;
Loading