Skip to content

Commit 496bc20

Browse files
committed
feat: parse javascript sources
1 parent 9b3eabd commit 496bc20

File tree

6 files changed

+133
-22
lines changed

6 files changed

+133
-22
lines changed

lib/nuke.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
const Source = require('./sources/source')
2+
const JsSource = require('./sources/js-source')
23
const CssNuker = require('./css-nuker')
34

45
function toSource(source) {
56
if (typeof source === 'string') {
67
return new Source(source)
78
} else if (typeof source === 'object') {
9+
if (source.type === 'js') {
10+
return new JsSource(source.content)
11+
}
12+
813
return new Source(source.content)
914
} else {
1015
throw new Error(`invalid source: ${source}`)

lib/sources/js-source.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
const esprima = require('esprima')
2+
const Source = require('./source')
3+
4+
class JsSource extends Source {
5+
constructor(text) {
6+
super(text)
7+
8+
try {
9+
const tokens = esprima.tokenize(text)
10+
.filter(token => token.type === 'String')
11+
.map(token => token.value.substr(1, token.value.length - 2))
12+
this._tokensArray = tokens
13+
this._tokens = new Set(tokens)
14+
} catch (err) {
15+
console.warn(err)
16+
}
17+
}
18+
19+
_findWholeSelectorInTokens(selector) {
20+
return this._tokensArray.find(token => Source.textContains(token, selector))
21+
}
22+
23+
_findSelectorPartsInTokens(selector) {
24+
if (selector.length === 0) {
25+
return true
26+
}
27+
28+
for (let i = 1; i <= selector.length; i++) {
29+
const part = selector.substr(0, i)
30+
const rest = selector.substr(i)
31+
if (this._tokens.has(part) && this._findSelectorPartsInTokens(rest)) {
32+
return true
33+
}
34+
}
35+
36+
return false
37+
}
38+
39+
contains(selector) {
40+
if (this._tokens) {
41+
return Boolean(this._tokens.has(selector) ||
42+
this._findWholeSelectorInTokens(selector) ||
43+
this._findSelectorPartsInTokens(selector))
44+
} else {
45+
return Source.textContains(this._text, selector)
46+
}
47+
}
48+
}
49+
50+
module.exports = JsSource

lib/sources/source.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ class Source {
44
}
55

66
contains(selector) {
7-
return new RegExp(`\\b${selector}\\b`, 'i').test(this._text)
7+
return Source.textContains(this._text, selector)
8+
}
9+
10+
static textContains(text, selector) {
11+
return new RegExp(`\\b${selector}\\b`, 'i').test(text)
812
}
913
}
1014

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
}
4141
},
4242
"dependencies": {
43+
"esprima": "^3.1.3",
4344
"gonzales-pe": "^4.0.3",
4445
"lodash": "^4.17.4"
4546
},

test/nuke.test.js

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,81 @@
11
const fs = require('fs')
22
const path = require('path')
3-
const nuke = require('../lib/nuke')
3+
const nukecss = require('../lib/nuke')
44

55
describe('nuke.js', function () {
6-
const htmlContent = fs.readFileSync(path.join(__dirname, '/fixtures/content.html'), 'utf8')
7-
const cssContent = fs.readFileSync(path.join(__dirname, '/fixtures/content.css'), 'utf8')
8-
9-
it('should remove unused rules', function () {
10-
const result = nuke(htmlContent, cssContent)
11-
expect(result).to.not.contain('foobar[something=x]')
12-
expect(result).to.not.contain('.something3')
13-
expect(result).to.not.contain('.foo-bar-3')
14-
})
6+
context('when content is basic', () => {
7+
const htmlContent = fs.readFileSync(path.join(__dirname, '/fixtures/content.html'), 'utf8')
8+
const cssContent = fs.readFileSync(path.join(__dirname, '/fixtures/content.css'), 'utf8')
159

16-
it('should remove empty media sets', function () {
17-
const result = nuke(htmlContent, cssContent)
18-
expect(result).to.not.contain('@media')
19-
})
10+
it('should remove unused rules', function () {
11+
const result = nukecss(htmlContent, cssContent)
12+
expect(result).to.not.contain('foobar[something=x]')
13+
expect(result).to.not.contain('.something3')
14+
expect(result).to.not.contain('.foo-bar-3')
15+
})
16+
17+
it('should remove empty media sets', function () {
18+
const result = nukecss(htmlContent, cssContent)
19+
expect(result).to.not.contain('@media')
20+
})
2021

21-
it('should respect nukecss:* comments', function () {
22-
const result = nuke(htmlContent, cssContent)
23-
expect(result).to.contain('.totally-unused')
22+
it('should respect nukecss:* comments', function () {
23+
const result = nukecss(htmlContent, cssContent)
24+
expect(result).to.contain('.totally-unused')
25+
})
26+
27+
it('should support multiple sources', function () {
28+
const result = nukecss([htmlContent, '<div id="foo-bar-3"></div>'], cssContent)
29+
expect(result).to.contain('.foo-bar-3')
30+
})
2431
})
2532

26-
it('should support multiple sources', function () {
27-
const result = nuke([htmlContent, '<div id="foo-bar-3"></div>'], cssContent)
28-
expect(result).to.contain('.foo-bar-3')
33+
context('when content is parseable', () => {
34+
const jsContent = 'const jsignored = "js-class other-class"'
35+
const moreJsContent = 'const woah = ["still", "works"].join("-")'
36+
const htmlContent = '<div id="primary" class="html-class">html-ignored</div>'
37+
const cssContent = `
38+
.jsignored { color: white; }
39+
.html-ignored { color: white; }
40+
.js-class { color: white; }
41+
.other-class { color: white; }
42+
.still-works { color: white; }
43+
.html-class { color: white; }
44+
#primary { color: white; }
45+
#primary > .unused { color: white; }
46+
.also-unused { color: white; }
47+
`.replace(/\n\s*/g, '\n')
48+
49+
const nuked = nukecss([
50+
{content: jsContent, type: 'js'},
51+
{content: moreJsContent, type: 'js'},
52+
{content: htmlContent, type: 'html'},
53+
], cssContent)
54+
55+
console.log(nuked)
56+
57+
it('should remove unused rules', function () {
58+
expect(nuked).to.not.contain('#primary > .unused')
59+
expect(nuked).to.not.contain('.also-unused')
60+
})
61+
62+
it('should not remove used rules', function () {
63+
expect(nuked).to.contain('.js-class')
64+
expect(nuked).to.contain('.other-class')
65+
expect(nuked).to.contain('.html-class')
66+
expect(nuked).to.contain('#primary')
67+
})
68+
69+
it('should remove unused rules mentioned in non-strings', function () {
70+
expect(nuked).to.not.contain('jsignored')
71+
})
72+
73+
it.skip('should remove unused rules mentioned in textnodes', function () {
74+
expect(nuked).to.not.contain('html-ignored')
75+
})
76+
77+
it('should not remove unused rules dynamically joined', function () {
78+
expect(nuked).to.contain('.still-works')
79+
})
2980
})
3081
})

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,7 @@ esprima@2.7.x, esprima@^2.7.1:
812812
version "2.7.3"
813813
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
814814

815-
esprima@^3.1.1:
815+
esprima@^3.1.1, esprima@^3.1.3:
816816
version "3.1.3"
817817
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
818818

0 commit comments

Comments
 (0)