Skip to content

Commit e8c1142

Browse files
authored
feat(icons): add linter to standard n actions icons (#5077)
* feat(icons): add linter to standard n actions icons * chore: remove comments * chore: update linter with new colors * chore: update recommended color set * chore: revert icon lint script * update script to evaulate changes * chore: move recommended colors to a separate file * chore: update the color codes to references
1 parent ea77d0b commit e8c1142

File tree

5 files changed

+169
-2
lines changed

5 files changed

+169
-2
lines changed

assets/icon-colors/icon-colors.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"description": "Recommended Colors for Standard and Action Icons. These colors will be used by icon-linter to verify bg-standard.yml and bg-actions.yml files",
3+
"source": "https://docs.google.com/presentation/d/1sZfkUjVIgdvCFac8O3FbLimkUSw9QeVJ8AgZ4fg4QlU/edit#slide=id.g13a86adcae5_0_0",
4+
"recommendedColors": [
5+
"green-60",
6+
"green-40",
7+
"indigo-50",
8+
"indigo-30",
9+
"hot-orange-60",
10+
"hot-orange-30",
11+
"teal-60",
12+
"teal-40",
13+
"purple-50",
14+
"purple-30",
15+
"neutral-60",
16+
"neutral-40",
17+
"cloud-blue-50",
18+
"cloud-blue-30",
19+
"violet-60",
20+
"violet-40",
21+
"blue-60",
22+
"blue-20",
23+
"pink-60",
24+
"pink-40"
25+
],
26+
"specialColors": [
27+
"#4A154B",
28+
"#481A54",
29+
"#0F1E3C",
30+
"#22683E"
31+
]
32+
}

gulpfile.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ gulp.task('lint:javascript:test', lint.javascriptTest);
159159
gulp.task('lint:html', lint.html);
160160
gulp.task('lint:markup', lint.markup);
161161
gulp.task('lint:tokens:yml', lint.tokensYml);
162+
gulp.task('lint:icons:yml', lint.iconsYml);
162163
gulp.task('lint:tokens:components', lint.tokensComponents);
163164
gulp.task('lint:tokens:aliases', lint.tokensAliases);
164165

@@ -171,6 +172,13 @@ gulp.task(
171172
)
172173
);
173174

175+
gulp.task(
176+
'lint:icons',
177+
gulp.parallel(
178+
withName('lint:icons:yml')(lint.iconsYml)
179+
)
180+
);
181+
174182
gulp.task(
175183
'lint:examples',
176184
gulp.series(
@@ -196,7 +204,8 @@ gulp.task(
196204
withName('lint:spaces')(lint.spaces),
197205
withName('lint:javascript')(lint.javascript),
198206
withName('lint:javascript:test')(lint.javascriptTest),
199-
'lint:tokens'
207+
'lint:tokens',
208+
'lint:icons'
200209
)
201210
);
202211

scripts/gulp/lint/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import eslint from 'gulp-eslint';
88
import stylelint from 'gulp-stylelint';
99
import htmlhint from 'gulp-htmlhint';
1010
import yamlValidate from './validate-yaml.js';
11-
11+
import iconlint from '../plugins/lint-icons';
1212
import tokenlint from '../plugins/lint-tokens';
1313

1414
import validateMarkup from './validate-markup';
@@ -135,3 +135,12 @@ export const tokensAliases = () =>
135135
.src(['./design-tokens/aliases/*.yml'])
136136
.pipe(tokenlint({ prefix: false }))
137137
.pipe(tokenlint.report('verbose'));
138+
139+
/**
140+
* To ensure that only the recommended colors are used for standard and action icons, a linter is being used to evaluate them
141+
*/
142+
export const iconsYml = () =>
143+
gulp
144+
.src(['./design-tokens/bg-standard.yml', './design-tokens/bg-actions.yml'])
145+
.pipe(iconlint())
146+
.pipe(iconlint.report());

scripts/gulp/plugins/lint-icons.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import paths from './../../helpers/paths';
2+
const path = require('path');
3+
const yaml = require('js-yaml');
4+
const map = require('map-stream');
5+
const _ = require('lodash');
6+
const gutil = require('gulp-util');
7+
const sdsColorPalette = require(`${path.resolve(paths.sdsStylingAliases, 'color-palettes.json')}`).aliases;
8+
const recommendedIconColors = require(path.resolve(__dirname, '../../../assets/icon-colors/icon-colors.json'));
9+
10+
const recommendedColorsHexCodes = recommendedIconColors.recommendedColors
11+
.map(color => `PALETTE_${color.replace(/-/g, '_').toUpperCase()}`)
12+
.map(color => sdsColorPalette[color].toUpperCase());
13+
const ICONS_COLORS = [...recommendedColorsHexCodes, ...recommendedIconColors.specialColors];
14+
const ICON_TYPES_TO_CHECK = ["standard", "actions"];
15+
16+
const verboseReporter = (lint, file) =>
17+
lint.errors.forEach(error => {
18+
const errors = Array.from(error.error).map(err => `\t❌ ${err}`).join('\n');
19+
const icon = error.icon;
20+
const filePath = file.path;
21+
const message = `${icon} in ${filePath} has following error(s): \n${errors}`;
22+
23+
gutil.log('iconlint: ', `${message}`);
24+
});
25+
26+
const IconLint = function (icons, pluginOptions = {}) {
27+
let self = this;
28+
29+
if (!(self instanceof IconLint)) {
30+
return new IconLint(icons, pluginOptions);
31+
}
32+
33+
self.errors = [];
34+
35+
_.keys(icons).forEach(icon => {
36+
const iconName = icon;
37+
const iconColor = icons[icon].value;
38+
try {
39+
self.iconColorAndCaseLint(iconName, iconColor, pluginOptions.type);
40+
} catch (e) {
41+
self.errors.push({
42+
icon: iconName,
43+
error: e
44+
});
45+
}
46+
});
47+
48+
return {
49+
errors: self.errors
50+
};
51+
};
52+
53+
IconLint.prototype.iconColorAndCaseLint = function (iconName, iconColor, iconType) {
54+
const errors = [];
55+
56+
if (!iconName || iconName.trim().length === 0 || !iconColor || iconColor.trim().length === 0) {
57+
errors.push('Icon name or color cannot be empty');
58+
}
59+
60+
if (iconName.toUpperCase() !== iconName) {
61+
errors.push('Icon names should be in uppercase');
62+
}
63+
64+
if (iconColor.toUpperCase() !== iconColor) {
65+
errors.push('Icon color code should be in uppercase');
66+
}
67+
68+
if (!_.includes(ICONS_COLORS, iconColor.toUpperCase())) {
69+
errors.push(`Icon color should be part of recommended colors - [${ICONS_COLORS}]`,
70+
`Please add the icon color to the recommended list if it is meant to be included`
71+
);
72+
}
73+
74+
if (errors.length) {
75+
throw errors;
76+
}
77+
};
78+
79+
const iconlintPlugin = (pluginOptions = {}) =>
80+
map(function (file, cb) {
81+
const iconType = path.parse(file.path).name.replace('bg-', '');
82+
if (ICON_TYPES_TO_CHECK.includes(iconType)) {
83+
pluginOptions.type = iconType;
84+
const icons = yaml.safeLoad(file.contents.toString('utf8'));
85+
const iconList = icons.props ? icons.props : [];
86+
const result = IconLint(iconList, pluginOptions);
87+
file.iconlint = {
88+
errors: result.errors
89+
};
90+
}
91+
92+
// Pass file
93+
cb(null, file);
94+
});
95+
96+
iconlintPlugin.report = _ =>
97+
map(function (file, cb) {
98+
let error = null;
99+
const iconlintErrors = file.iconlint ? file.iconlint.errors : null;
100+
if (iconlintErrors) {
101+
if (iconlintErrors.length) {
102+
error = new gutil.PluginError(
103+
'iconlint', `Found ${iconlintErrors.length} linting error(s)`
104+
);
105+
}
106+
verboseReporter(file.iconlint, file);
107+
}
108+
109+
// Pass file
110+
cb(error, file);
111+
});
112+
113+
module.exports = iconlintPlugin;

scripts/helpers/paths.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ module.exports = {
4242
node_modules,
4343
'@salesforce-ux/sds-styling-hooks/src/props'
4444
),
45+
sdsStylingAliases: path.resolve(
46+
node_modules,
47+
'@salesforce-ux/sds-styling-aliases/dist'
48+
),
4549
icons: path.resolve(
4650
node_modules,
4751
'@salesforce-ux/icons/dist/salesforce-lightning-design-system-icons'

0 commit comments

Comments
 (0)