Skip to content

Commit

Permalink
build: convert stylelint rules to typescript (angular#19047)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto authored Apr 17, 2020
1 parent f020403 commit 313e3f3
Show file tree
Hide file tree
Showing 11 changed files with 636 additions and 438 deletions.
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

0 comments on commit 313e3f3

Please sign in to comment.