@projectwallace/css-analyzer 5.0.0-alpha.1 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/analyzer.cjs +1 -1
  2. package/dist/analyzer.cjs.map +1 -1
  3. package/dist/analyzer.modern.js +2 -0
  4. package/dist/analyzer.modern.js.map +1 -0
  5. package/dist/analyzer.module.js +1 -1
  6. package/dist/analyzer.module.js.map +1 -1
  7. package/dist/analyzer.umd.js +1 -1
  8. package/dist/analyzer.umd.js.map +1 -1
  9. package/package.json +10 -10
  10. package/dist/analyzer.js +0 -2
  11. package/dist/analyzer.js.map +0 -1
  12. package/src/aggregate-collection.js +0 -113
  13. package/src/aggregate-collection.test.js +0 -47
  14. package/src/atrules/atrules.js +0 -76
  15. package/src/atrules/atrules.test.js +0 -289
  16. package/src/context-collection.js +0 -36
  17. package/src/countable-collection.js +0 -46
  18. package/src/declarations/declarations.js +0 -46
  19. package/src/declarations/declarations.test.js +0 -113
  20. package/src/index.js +0 -259
  21. package/src/index.test.js +0 -60
  22. package/src/properties/properties.js +0 -48
  23. package/src/properties/properties.test.js +0 -138
  24. package/src/rules/rules.js +0 -57
  25. package/src/rules/rules.test.js +0 -247
  26. package/src/selectors/complexity.test.js +0 -123
  27. package/src/selectors/selectors.js +0 -122
  28. package/src/selectors/selectors.test.js +0 -189
  29. package/src/selectors/specificity.js +0 -201
  30. package/src/selectors/specificity.test.js +0 -247
  31. package/src/smoke.test.js +0 -39
  32. package/src/stylesheet/stylesheet.test.js +0 -88
  33. package/src/values/animations.js +0 -53
  34. package/src/values/animations.test.js +0 -154
  35. package/src/values/box-shadows.test.js +0 -82
  36. package/src/values/colors.js +0 -192
  37. package/src/values/colors.test.js +0 -804
  38. package/src/values/font-families.js +0 -98
  39. package/src/values/font-families.test.js +0 -119
  40. package/src/values/font-sizes.js +0 -92
  41. package/src/values/font-sizes.test.js +0 -120
  42. package/src/values/text-shadows.test.js +0 -93
  43. package/src/values/units.test.js +0 -72
  44. package/src/values/values.js +0 -30
  45. package/src/values/vendor-prefix.js +0 -45
  46. package/src/values/vendor-prefix.test.js +0 -64
  47. package/src/values/z-index.test.js +0 -54
  48. package/src/vendor-prefix.js +0 -16
package/src/index.js DELETED
@@ -1,259 +0,0 @@
1
- import * as csstree from 'css-tree'
2
- import { compareSpecificity } from './selectors/specificity.js'
3
- import { analyzeRules } from './rules/rules.js'
4
- import { analyzeColors, colorFunctions, colorNames } from './values/colors.js'
5
- import { analyzeFontFamilies } from './values/font-families.js'
6
- import { analyzeFontSizes } from './values/font-sizes.js'
7
- import { analyzeDeclarations } from './declarations/declarations.js'
8
- import { analyzeSelectors } from './selectors/selectors.js'
9
- import { analyzeProperties } from './properties/properties.js'
10
- import { analyzeValues } from './values/values.js'
11
- import { analyzeAnimations } from './values/animations.js'
12
- import { analyzeVendorPrefixes } from './values/vendor-prefix.js'
13
- import { analyzeAtRules } from './atrules/atrules.js'
14
- import { CountableCollection } from './countable-collection.js'
15
- import { ContextCollection } from './context-collection.js'
16
-
17
- /**
18
- *
19
- * @param {string} css
20
- * @returns
21
- */
22
- const analyze = (css) => {
23
- const start = new Date()
24
-
25
- // We need all lines later on when we need to stringify the AST again
26
- // e.g. for Selectors
27
- const lines = css.split(/\r?\n/)
28
-
29
- /**
30
- * Recreate the authored CSS from a CSSTree node
31
- * @param {import('css-tree').Node} node - Node from CSSTree AST to stringify
32
- * @returns {string} str - The stringified node
33
- */
34
- function stringifyNode(node) {
35
- const start = node.loc.start
36
- const end = node.loc.end
37
- const lineCount = end.line - start.line
38
-
39
- // Single-line nodes
40
- if (lineCount === 0) {
41
- return lines[start.line - 1].substring(start.column - 1, end.column - 1)
42
- }
43
-
44
- // Multi-line nodes
45
- const value = []
46
-
47
- for (let i = start.line; i <= end.line; i++) {
48
- const line = lines[i - 1]
49
- // First line
50
- if (i === start.line) {
51
- value.push(line.substring(start.column - 1))
52
- continue
53
- }
54
- // Last line
55
- if (i === end.line) {
56
- value.push(line.substring(0, end.column - 1))
57
- continue
58
- }
59
- // All lines in between first and last
60
- value.push(line)
61
- }
62
-
63
- return value.join('\n')
64
- }
65
-
66
- const startParse = new Date()
67
- let totalComments = 0
68
- let commentsSize = 0
69
-
70
- const ast = csstree.parse(css, {
71
- parseAtrulePrelude: false,
72
- parseCustomProperty: true, // To find font-families, colors, etc.
73
- positions: true, // So we can use stringifyNode()
74
- onComment: function (comment) {
75
- totalComments++
76
- commentsSize += comment.length
77
- },
78
- })
79
-
80
- const startAnalysis = new Date()
81
- const atrules = []
82
- const rules = []
83
- const selectors = []
84
- const declarations = []
85
- const properties = []
86
- const values = []
87
- const zindex = []
88
- const textShadows = []
89
- const boxShadows = []
90
- const fontValues = []
91
- const fontFamilyValues = []
92
- const fontSizeValues = []
93
- const animations = []
94
- const timingFunctions = []
95
- const durations = []
96
- const colors = new ContextCollection()
97
- const units = new ContextCollection()
98
-
99
- csstree.walk(ast, {
100
- enter: function (node) {
101
- switch (node.type) {
102
- case 'Atrule': {
103
- atrules.push(node)
104
- break
105
- }
106
- case 'Rule': {
107
- rules.push(node)
108
- break
109
- }
110
- case 'Selector': {
111
- selectors.push({
112
- ...node,
113
- isKeyframeSelector: this.atrule && this.atrule.name.endsWith('keyframes')
114
- })
115
-
116
- // Avoid further walking of selectors to not mess with
117
- // our specificity calculations in case of a selector
118
- // with :where() or :is() that contain SelectorLists
119
- // as children
120
- return this.skip
121
- }
122
- case 'Dimension': {
123
- if (!this.declaration) {
124
- break
125
- }
126
-
127
- units.push(node.unit, this.declaration.property)
128
-
129
- return this.skip
130
- }
131
- case 'Declaration': {
132
- declarations.push({
133
- ...node,
134
- inKeyframe: this.atrule && this.atrule.name.endsWith('keyframes')
135
- })
136
-
137
- const { value, property } = node
138
- const fullProperty = {
139
- authored: property,
140
- ...csstree.property(property)
141
- }
142
-
143
- properties.push(fullProperty)
144
- values.push(value)
145
-
146
- switch (fullProperty.basename) {
147
- case 'z-index': {
148
- zindex.push(value)
149
- break
150
- }
151
- case 'text-shadow': {
152
- textShadows.push(value)
153
- break
154
- }
155
- case 'box-shadow': {
156
- boxShadows.push(value)
157
- break
158
- }
159
- case 'font': {
160
- fontValues.push(value)
161
- break
162
- }
163
- case 'font-family': {
164
- fontFamilyValues.push(stringifyNode(value))
165
- // Prevent analyzer to find color names in this property
166
- return this.skip
167
- }
168
- case 'font-size': {
169
- fontSizeValues.push(stringifyNode(value))
170
- break
171
- }
172
- case 'transition':
173
- case 'animation': {
174
- animations.push(node)
175
- break
176
- }
177
- case 'animation-duration':
178
- case 'transition-duration': {
179
- durations.push(value)
180
- break
181
- }
182
- case 'transition-timing-function':
183
- case 'animation-timing-function': {
184
- timingFunctions.push(value)
185
- break
186
- }
187
- }
188
-
189
- csstree.walk(node.value, {
190
- enter: function (valueNode) {
191
- switch (valueNode.type) {
192
- case 'Hash': {
193
- colors.push(stringifyNode(valueNode), property)
194
-
195
- return this.skip
196
- }
197
- case 'Identifier': {
198
- const { name } = valueNode
199
- // Bail out if it can't be a color name
200
- // 20 === 'lightgoldenrodyellow'.length
201
- // 3 === 'red'.length
202
- if (name.length > 20 || name.length < 3) {
203
- return this.skip
204
- }
205
- if (colorNames[name.toLowerCase()]) {
206
- colors.push(stringifyNode(valueNode), property)
207
- }
208
- return this.skip
209
- }
210
- case 'Function': {
211
- if (colorFunctions[valueNode.name.toLowerCase()]) {
212
- colors.push(stringifyNode(valueNode), property)
213
- }
214
- // No this.skip here intentionally,
215
- // otherwise we'll miss colors in linear-gradient() etc.
216
- }
217
- }
218
- }
219
- })
220
- }
221
- }
222
- }
223
- })
224
-
225
- return {
226
- stylesheet: {
227
- sourceLinesOfCode: atrules.length + selectors.length + declarations.length,
228
- linesOfCode: lines.length,
229
- size: css.length,
230
- comments: {
231
- total: totalComments,
232
- size: commentsSize,
233
- },
234
- },
235
- atrules: analyzeAtRules({ atrules, stringifyNode }),
236
- rules: analyzeRules({ rules }),
237
- selectors: analyzeSelectors({ stringifyNode, selectors }),
238
- declarations: analyzeDeclarations({ stringifyNode, declarations }),
239
- properties: analyzeProperties({ properties }),
240
- values: {
241
- colors: analyzeColors({ colors }),
242
- fontFamilies: analyzeFontFamilies({ stringifyNode, fontValues, fontFamilyValues }),
243
- fontSizes: analyzeFontSizes({ stringifyNode, fontValues, fontSizeValues }),
244
- zindexes: analyzeValues({ values: zindex, stringifyNode }),
245
- textShadows: analyzeValues({ values: textShadows, stringifyNode }),
246
- boxShadows: analyzeValues({ values: boxShadows, stringifyNode }),
247
- animations: analyzeAnimations({ animations, timingFunctions, durations, stringifyNode }),
248
- prefixes: analyzeVendorPrefixes({ values, stringifyNode }),
249
- units: units.count(),
250
- },
251
- __meta__: {
252
- parseTime: startAnalysis - startParse,
253
- analyzeTime: new Date() - startAnalysis,
254
- total: new Date() - start
255
- }
256
- }
257
- }
258
-
259
- export { analyze, compareSpecificity }
package/src/index.test.js DELETED
@@ -1,60 +0,0 @@
1
- import { suite } from 'uvu';
2
- import * as assert from 'uvu/assert';
3
- import { analyze, compareSpecificity } from './index.js'
4
- import { readFileSync } from 'fs'
5
-
6
-
7
- const fixtures = [
8
- 'bol-com-20190617.css',
9
- 'css-tricks-20190319.css',
10
- 'facebook-20190319.css',
11
- 'gazelle-20210905.css',
12
- 'github-20210501.css',
13
- 'lego-20190617.css',
14
- 'smashing-magazine-20190319.css',
15
- 'trello-20190617.css',
16
- ].map(fileName => {
17
- const css = readFileSync(`./src/__fixtures__/${fileName}`, 'utf-8')
18
- return {
19
- css,
20
- fileName
21
- }
22
- })
23
-
24
- const Api = suite('Public API')
25
-
26
- Api('exposes the analyze method', () => {
27
- assert.is(typeof compareSpecificity, 'function')
28
- })
29
-
30
- Api('exposes the compareSpecificity method', () => {
31
- assert.is(typeof compareSpecificity, 'function')
32
- })
33
-
34
- Api('does not break on CSS Syntax Errors', () => {
35
- assert.not.throws(() => analyze('test, {}'))
36
- assert.not.throws(() => analyze('test { color red }'))
37
- })
38
-
39
- Api('serializes into small objects', () => {
40
- for (const { css, fileName } of fixtures) {
41
- const actual = JSON.stringify(analyze(css))
42
- assert.ok(
43
- actual.length < css.length * 0.51,
44
- `Expected serialized size to be smaller (${actual.length} serialized vs. ${css.length} raw css (${actual.length / css.length}) on ${fileName})`
45
- )
46
- }
47
- })
48
-
49
- Api('it runs fast', () => {
50
- const start = Date.now()
51
- for (const { css } of fixtures) {
52
- analyze(css)
53
- }
54
- const end = Date.now()
55
- const actual = end - start
56
-
57
- assert.ok(actual < 2500, `Expected to be fast, but took ${actual}ms`)
58
- })
59
-
60
- Api.run()
@@ -1,48 +0,0 @@
1
- import { CountableCollection } from '../countable-collection.js'
2
-
3
- const analyzeProperties = ({ properties }) => {
4
- const all = new CountableCollection(properties.map(p => p.authored))
5
- const prefixed = new CountableCollection()
6
- const hacks = new CountableCollection()
7
- const customs = new CountableCollection()
8
- const totalProperties = properties.length
9
-
10
- for (let i = 0; i < totalProperties; i++) {
11
- const property = properties[i]
12
-
13
- if (property.vendor) {
14
- prefixed.push(property.authored)
15
- continue
16
- }
17
-
18
- if (property.hack) {
19
- hacks.push(property.authored)
20
- continue
21
- }
22
-
23
- if (property.custom) {
24
- customs.push(property.authored)
25
- continue
26
- }
27
- }
28
-
29
- return {
30
- ...all.count(),
31
- prefixed: {
32
- ...prefixed.count(),
33
- ratio: prefixed.size() / totalProperties,
34
- },
35
- custom: {
36
- ...customs.count(),
37
- ratio: customs.size() / totalProperties,
38
- },
39
- browserhacks: {
40
- ...hacks.count(),
41
- ratio: hacks.size() / totalProperties,
42
- }
43
- }
44
- }
45
-
46
- export {
47
- analyzeProperties
48
- }
@@ -1,138 +0,0 @@
1
- import { suite } from 'uvu'
2
- import * as assert from 'uvu/assert'
3
- import { analyze } from '../index.js'
4
-
5
- const Properties = suite('Properties')
6
-
7
- Properties('counts totals', () => {
8
- const fixture = `
9
- properties {
10
- margin: 0;
11
- --custom: 1;
12
- }
13
-
14
- @media print {
15
- nested {
16
- --custom: 2;
17
- }
18
- }
19
- `
20
- const actual = analyze(fixture).properties.total
21
-
22
- assert.is(actual, 3)
23
- })
24
-
25
- Properties('calculates uniqueness', () => {
26
- const fixture = `
27
- properties {
28
- margin: 0;
29
- --custom: 1;
30
- }
31
-
32
- @media print {
33
- nested {
34
- --custom: 1;
35
- }
36
- }
37
- `
38
- const actual = analyze(fixture).properties
39
- const expected = {
40
- 'margin': 1,
41
- '--custom': 2,
42
- }
43
-
44
- assert.is(actual.totalUnique, 2)
45
- assert.equal(actual.unique, expected)
46
- assert.is(actual.uniquenessRatio, 2 / 3)
47
- })
48
-
49
- Properties('counts vendor prefixes', () => {
50
- const fixture = `
51
- prefixed {
52
- border-radius: 2px;
53
- -webkit-border-radius: 2px;
54
- -khtml-border-radius: 2px;
55
- -o-border-radius: 2px;
56
- }
57
-
58
- @media (min-width: 0) {
59
- @supports (-o-border-radius: 2px) {
60
- prefixed2 {
61
- -o-border-radius: 4px;
62
- }
63
- }
64
- }
65
- `
66
- const actual = analyze(fixture).properties.prefixed
67
- const expected = {
68
- '-webkit-border-radius': 1,
69
- '-khtml-border-radius': 1,
70
- '-o-border-radius': 2,
71
- }
72
-
73
- assert.is(actual.total, 4)
74
- assert.is(actual.totalUnique, 3)
75
- assert.equal(actual.unique, expected)
76
- assert.is(actual.ratio, 4 / 5)
77
- })
78
-
79
- Properties('counts browser hacks', () => {
80
- const fixture = `
81
- hacks {
82
- margin: 0;
83
- *zoom: 1;
84
- }
85
-
86
- @media (min-width: 0) {
87
- @supports (-o-border-radius: 2px) {
88
- hacks2 {
89
- *zoom: 1;
90
- }
91
- }
92
- }
93
- `
94
- const actual = analyze(fixture).properties.browserhacks
95
- const expected = {
96
- '*zoom': 2
97
- }
98
-
99
- assert.is(actual.total, 2)
100
- assert.is(actual.totalUnique, 1)
101
- assert.equal(actual.unique, expected)
102
- assert.is(actual.ratio, 2 / 3)
103
- })
104
-
105
- Properties('counts custom properties', () => {
106
- const fixture = `
107
- :root {
108
- --yellow-400: yellow;
109
- }
110
-
111
- custom {
112
- margin: 0;
113
- --yellow-400: yellow;
114
- color: var(--yellow-400);
115
- }
116
-
117
- @media (min-width: 0) {
118
- @supports (-o-border-radius: 2px) {
119
- custom2 {
120
- --green-400: green;
121
- color: var(--green-400);
122
- }
123
- }
124
- }
125
- `
126
- const actual = analyze(fixture).properties.custom
127
- const expected = {
128
- '--yellow-400': 2,
129
- '--green-400': 1,
130
- }
131
-
132
- assert.is(actual.total, 3)
133
- assert.is(actual.totalUnique, 2)
134
- assert.equal(actual.unique, expected)
135
- assert.is(actual.ratio, 3 / 6)
136
- })
137
-
138
- Properties.run()
@@ -1,57 +0,0 @@
1
- import * as csstree from 'css-tree'
2
- import { AggregateCollection } from '../aggregate-collection.js'
3
-
4
- const analyzeRules = ({ rules }) => {
5
- const totalRules = rules.length
6
- const selectorsPerRule = new AggregateCollection(totalRules)
7
- const declarationsPerRule = new AggregateCollection(totalRules)
8
-
9
- let emptyRules = 0
10
-
11
- for (let i = 0; i < totalRules; i++) {
12
- let selectors = 0
13
- let declarations = 0
14
-
15
- csstree.walk(rules[i], {
16
- enter: function (childNode) {
17
- if (childNode.type === 'Selector') {
18
- selectors++
19
- return this.skip
20
- }
21
-
22
- if (childNode.type === 'Declaration') {
23
- declarations++
24
- return this.skip
25
- }
26
- }
27
- })
28
-
29
- if (declarations === 0) {
30
- emptyRules++
31
- }
32
-
33
- // For later aggregations
34
- selectorsPerRule.add(selectors)
35
- declarationsPerRule.add(declarations)
36
- }
37
-
38
- return {
39
- total: totalRules,
40
- empty: {
41
- total: emptyRules,
42
- ratio: emptyRules / totalRules
43
- },
44
- selectors: {
45
- ...selectorsPerRule.aggregate(),
46
- items: selectorsPerRule.toArray(),
47
- },
48
- declarations: {
49
- ...declarationsPerRule.aggregate(),
50
- items: declarationsPerRule.toArray()
51
- },
52
- }
53
- }
54
-
55
- export {
56
- analyzeRules
57
- }