1 | #!/usr/bin/env node
|
2 |
|
3 | const AssetGraph = require('../lib/AssetGraph');
|
4 | const _ = require('lodash');
|
5 | const urlTools = require('urltools');
|
6 | const commandLineOptions = require('yargs')
|
7 | .usage('$0 [--root <inputRootDirectory>] <htmlFile>...')
|
8 | .demand(1).argv;
|
9 |
|
10 | const ignorePseudoClasses = [
|
11 | 'active',
|
12 | 'checked',
|
13 | 'default',
|
14 | 'disabled',
|
15 | 'empty',
|
16 | 'enabled',
|
17 | 'first',
|
18 | 'fullscreen',
|
19 | 'focus',
|
20 | 'hover',
|
21 | 'indeterminate',
|
22 | 'in-range',
|
23 | 'invalid',
|
24 | 'visited',
|
25 | 'left',
|
26 | 'right',
|
27 | 'read-only',
|
28 | 'read-write',
|
29 | 'optional',
|
30 | 'out-of-range',
|
31 | 'link',
|
32 | ];
|
33 |
|
34 | // Pseudo-classes intentionally not on the above list because they make sense
|
35 | // on a non-live document HTML:
|
36 | // first-child, first-of-type, last-child, last-of-type, only-child, only-of-type, required, root, scope, target, valid
|
37 |
|
38 | // Give up:
|
39 | // lang() not() nth-child() nth-last-child() nth-last-of-type() nth-of-type()
|
40 |
|
41 | new AssetGraph({ root: commandLineOptions.root })
|
42 | .logEvents({
|
43 | repl: commandLineOptions.repl,
|
44 | stopOnWarning: commandLineOptions.stoponwarning,
|
45 | suppressJavaScriptCommonJsRequireWarnings: true,
|
46 | })
|
47 | .loadAssets(commandLineOptions._.map(urlTools.fsFilePathToFileUrl))
|
48 | .populate({
|
49 | followRelations: {
|
50 | type: ['HtmlStyle', 'CssImport'],
|
51 | to: { url: /^file:/ },
|
52 | },
|
53 | })
|
54 | .queue(function (assetGraph) {
|
55 | var documents = _.map(assetGraph.findAssets({ type: 'Html' }), 'parseTree');
|
56 | function isSelectorUsed(selector) {
|
57 | var giveUp = false;
|
58 | // Preprocess pseudo-classes
|
59 | selector = selector.replace(
|
60 | /:([a-z-]+)(\()?/g,
|
61 | function ($0, pseudoClassName, leadingParen) {
|
62 | if (leadingParen) {
|
63 | // Give up on :not(...) etc., anything that takes parameters
|
64 | giveUp = true;
|
65 | return $0;
|
66 | } else if (ignorePseudoClasses.indexOf(pseudoClassName) !== -1) {
|
67 | return '';
|
68 | } else {
|
69 | return $0;
|
70 | }
|
71 | }
|
72 | );
|
73 | if (giveUp) {
|
74 | return true;
|
75 | }
|
76 |
|
77 | return documents.some(function (document) {
|
78 | try {
|
79 | return document.querySelectorAll(selector).length > 0;
|
80 | } catch (e) {
|
81 | // Assume that selectors jsdom doesn't support are in use:
|
82 | return true;
|
83 | }
|
84 | });
|
85 | }
|
86 | assetGraph.findAssets({ type: 'Css' }).forEach(function (cssAsset) {
|
87 | cssAsset.constructor.eachRuleInParseTree(
|
88 | cssAsset.parseTree,
|
89 | function (cssRule) {
|
90 | if (cssRule.type === 'rule') {
|
91 | // STYLE_RULE
|
92 | var selectors = cssRule.selectors;
|
93 | var unusedSelectors = cssRule.selectors.filter(function (selector) {
|
94 | return !isSelectorUsed(selector);
|
95 | });
|
96 | if (unusedSelectors.length > 0) {
|
97 | if (unusedSelectors.length === selectors.length) {
|
98 | console.warn('Unused rule: ' + cssRule.toString());
|
99 | } else {
|
100 | console.warn(
|
101 | 'Unused selector' +
|
102 | (unusedSelectors.length === 1 ? '' : 's') +
|
103 | ' "' +
|
104 | unusedSelectors.join('", "') +
|
105 | '" in rule: ' +
|
106 | cssRule.toString()
|
107 | );
|
108 | }
|
109 | }
|
110 | }
|
111 | }
|
112 | );
|
113 | });
|
114 | })
|
115 | .run();
|