├── test
├── integration
│ ├── data-js
│ │ ├── a.js
│ │ └── b.js
│ ├── data
│ │ ├── a
│ │ │ └── c.css
│ │ ├── a.html
│ │ ├── b.css
│ │ ├── b
│ │ │ └── d.css
│ │ └── a.css
│ ├── api.js.js
│ ├── api.errors.js
│ └── api.css.js
├── manual
│ ├── css
│ │ ├── a
│ │ │ └── c.css
│ │ ├── b.css
│ │ ├── b
│ │ │ └── d.css
│ │ └── a.css
│ ├── gfx
│ │ └── img.png
│ ├── processed.html
│ ├── unprocessed.html
│ └── index.html
├── mocha.opts
├── common.js
└── unit
│ ├── minifier.js
│ ├── utils.js
│ └── css.parser.js
├── .gitignore
├── .gitattributes
├── .npmignore
├── prepareManualTests.js
├── package.json
├── src
├── css.js
├── utils.js
└── minify.js
├── index.js
└── readme.md
/test/integration/data-js/a.js:
--------------------------------------------------------------------------------
1 | 'a';
2 | 'b';
3 |
--------------------------------------------------------------------------------
/test/integration/data-js/b.js:
--------------------------------------------------------------------------------
1 | 'c';
2 | 'd';
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *min.js
3 | *min.css
4 |
--------------------------------------------------------------------------------
/test/manual/css/a/c.css:
--------------------------------------------------------------------------------
1 | @import url(../b/d.css);
2 |
--------------------------------------------------------------------------------
/test/manual/css/b.css:
--------------------------------------------------------------------------------
1 | div {
2 | height: 160px;
3 | width: 202px;
4 | }
5 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/test/manual/css/b/d.css:
--------------------------------------------------------------------------------
1 | .nested-img {
2 | background: url(../../gfx/img.png);
3 | }
4 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --recursive
2 | --ui bdd
3 | --reporter dot
4 | --require test/common.js
5 |
--------------------------------------------------------------------------------
/test/manual/gfx/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fizker/minifier/HEAD/test/manual/gfx/img.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *min.js
3 | *min.css
4 | test
5 | prepareManualTests.js
6 | runAllTests.js
7 |
--------------------------------------------------------------------------------
/test/integration/data/a/c.css:
--------------------------------------------------------------------------------
1 | @import url(../b/d.css);
2 |
3 | .c-file {
4 | background: url(c-file);
5 | }
6 |
--------------------------------------------------------------------------------
/test/integration/data/a.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Test page
4 |
5 | This is a test page
6 |
--------------------------------------------------------------------------------
/test/integration/data/b.css:
--------------------------------------------------------------------------------
1 | div {
2 | height: 160px;
3 | width: 202px;
4 | }
5 |
6 | .b-file {
7 | background: url(b-file);
8 | }
9 |
--------------------------------------------------------------------------------
/test/manual/css/a.css:
--------------------------------------------------------------------------------
1 | @import url(b.css);
2 | @import url(a/c.css);
3 |
4 | .root-img {
5 | background: url(../gfx/img.png);
6 | }
7 |
--------------------------------------------------------------------------------
/test/integration/data/b/d.css:
--------------------------------------------------------------------------------
1 | .nested-img {
2 | background: url(../../gfx/img.png);
3 | }
4 |
5 | .d-file {
6 | background: url(d-file);
7 | }
8 |
--------------------------------------------------------------------------------
/test/common.js:
--------------------------------------------------------------------------------
1 | global.chai = require('chai');
2 | global.expect = chai.expect;
3 |
4 | global.fzkes = require('fzkes')
5 | chai.use(fzkes)
6 |
7 | require('finc-chai-helpers').addMethods(chai);
8 |
--------------------------------------------------------------------------------
/test/integration/data/a.css:
--------------------------------------------------------------------------------
1 | @import url(b.css);
2 | @import url(a/c.css);
3 |
4 | .root-img {
5 | background: url(../gfx/img.png);
6 | }
7 |
8 | .a-file {
9 | background: url(a-file);
10 | }
11 |
--------------------------------------------------------------------------------
/test/manual/processed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
--------------------------------------------------------------------------------
/test/manual/unprocessed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
--------------------------------------------------------------------------------
/prepareManualTests.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var path = require('path')
4 | var input = path.join(__dirname, 'test/manual/css/a.css')
5 | var output = path.join(__dirname, 'test/manual/min.css')
6 |
7 | var minifier = require('./index')
8 | minifier.minify(input, { output: output })
9 |
10 | console.log('The manual test is complete')
11 |
--------------------------------------------------------------------------------
/test/manual/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
17 |
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Benjamin Horsleben (http://fizkerinc.dk)",
3 | "name": "minifier",
4 | "description": "A simple tool for minifying CSS/JS without a big setup",
5 | "keywords": "javascript js css minify minifier build",
6 | "version": "0.8.1",
7 | "license": "WTFPL",
8 | "main": "index.js",
9 | "repository": {
10 | "type": "git",
11 | "url": "git://github.com/fizker/minifier.git"
12 | },
13 | "bugs": "https://github.com/fizker/minifier/issues",
14 | "dependencies": {
15 | "commander": "^2.8.1",
16 | "css-resolve-import": "^0.1.1",
17 | "fmerge": "^1.2.0",
18 | "glob": "^7.1.1",
19 | "hogan.js": "^3.0.2",
20 | "sqwish": "~0.2.2",
21 | "uglify-js": "^2.4.24"
22 | },
23 | "bin": {
24 | "minify": "index.js"
25 | },
26 | "scripts": {
27 | "test": "mocha"
28 | },
29 | "devDependencies": {
30 | "chai": "^3.2.0",
31 | "finc-chai-helpers": "~0.1.0",
32 | "fzkes": "^0.14.1",
33 | "mocha": "^3.2.0"
34 | },
35 | "optionalDependencies": {},
36 | "engines": {
37 | "node": "^4.4.7 || >=6.3.0"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/css.js:
--------------------------------------------------------------------------------
1 | module.exports =
2 | { parse: parse
3 | }
4 |
5 | var fs = require('fs')
6 | var path = require('path')
7 | var format = require('util').format
8 | var utils = require('./utils')
9 | var stringImportMatcher = /@import ["'](.+)["'];/g
10 | var importMatcher = /@import +(url\()?([^()]+)\)? *;/g
11 | var urlMatcher = /url\(["']?([^"'()]+)["']?\)/g
12 | var absoluteUrl = /^([a-zA-Z]:\/)?\//
13 | var dataUrl = /^data:/
14 |
15 | function parse(file, absRoot, minifier) {
16 | if(!minifier) minifier = function(content) { return content }
17 | var root = path.dirname(file)
18 | var absRoot = absRoot || ''
19 | var relRoot = path.relative(absRoot, root)
20 | var content = minifier(utils.stripUTF8ByteOrder(fs.readFileSync(file, 'utf8')))
21 |
22 | return content
23 | .replace(stringImportMatcher, function(match, url) {
24 | return format('@import url(%s);', url)
25 | })
26 | .replace(urlMatcher, function(match, url) {
27 | url = url.trim()
28 | if(!url.match(dataUrl) && !url.match(absoluteUrl)) {
29 | url = path.join(relRoot, url).replace(/\\/g, '/')
30 | }
31 | return format('url(%s)', url)
32 | })
33 | .replace(importMatcher, function(match, junk, file) {
34 | if(!file.match(absoluteUrl)) {
35 | file = path.join(absRoot, file)
36 | }
37 | var parsedFile = parse(file, absRoot)
38 | return parsedFile +'\n'
39 | })
40 | .trim()
41 | }
42 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | module.exports =
2 | { stripUTF8ByteOrder: stripUTF8ByteOrder
3 | , generateOutputName: generateOutputName
4 | }
5 |
6 | var format = require('util').format
7 | var hogan = require('hogan.js')
8 | var digest = require('crypto').createHash
9 | var path = require('path')
10 |
11 | function generateOutputName(input, options) {
12 | if(!options) options = {}
13 | var extractedInput =
14 | { md5: generate.bind(null, 'md5')
15 | , sha: generate.bind(null, 'sha256')
16 | }
17 | var dir = path.dirname(input)
18 | path.basename(input).replace(/^(.*)\.([^.]+)$/, function(match, file, ext) {
19 | extractedInput.ext = ext
20 | extractedInput.filename = file
21 | return ''
22 | })
23 |
24 | var output = hogan.compile(options.template || '{{filename}}.min.{{ext}}').render(extractedInput)
25 |
26 | output = path.join(dir, output)
27 |
28 | if(options.regex) return new RegExp(output.replace(/\.([^*])/g, '\\.$1'))
29 | return output
30 |
31 | function generate(algorithm) {
32 | if(options.regex) return '.*'
33 | if(options.glob) return '*'
34 | if(!options.content) throw new Error('Content is required for producing ' + algorithm)
35 | var digester = digest(algorithm)
36 | digester.update(options.content, 'utf8')
37 | return digester.digest('hex')
38 | }
39 | }
40 |
41 | function stripUTF8ByteOrder(data) {
42 | var content = data.toString()
43 | if(content[0] === '\uFEFF') {
44 | content = content.substring(1)
45 | }
46 | return content
47 | }
48 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var program = require('commander')
4 | var minifier = require('./src/minify')
5 |
6 | if(require.main === module) {
7 | var skip = []
8 | program
9 | .version(require('./package.json').version)
10 | .option('-o, --output [file]', 'The output file')
11 | .option('-t, --template [template]', 'A template for building the output file')
12 | .option('-c, --clean', 'Deletes any files that resembles the template')
13 | .option('-C, --clean-only', 'Same as `--clean`, but without minifying the files afterwards')
14 | .option('-s, --skip ', 'Skip any files that contains this in the path')
15 | .option('--no-comments', 'Remove license-style comments')
16 | .usage('[--output file] path/to/input [...path/to/other/input]')
17 |
18 | .on('skip', function(path) {
19 | skip.push(path)
20 | })
21 |
22 | .parse(process.argv)
23 |
24 | var inputs = program.args
25 | var input
26 | if(inputs.length == 1) {
27 | input = inputs[0]
28 | }
29 |
30 | if(inputs.length == 0) {
31 | program.parse(['bla', 'bla', '--help'])
32 | process.exit()
33 | }
34 |
35 | if(skip.length) program.skip = skip
36 |
37 | program.noComments = program.comments === false
38 |
39 | minifier.on('error', function(msg) {
40 | console.log(msg)
41 | process.exit(1)
42 | })
43 | program.uglify = {
44 | output: {
45 | semicolons:false,
46 | },
47 | }
48 | minifier.minify(input || inputs, program)
49 |
50 | if(program.cleanOnly) {
51 | return console.log('Minified files cleaned')
52 | }
53 |
54 | console.log('Minification complete')
55 | }
56 |
57 | module.exports = minifier
58 |
--------------------------------------------------------------------------------
/test/integration/api.js.js:
--------------------------------------------------------------------------------
1 | describe('integration/api.js.js', function() {
2 | var minifier = require('../../index')
3 | var fs = require('fs')
4 | var path = require('path')
5 | describe('When aimed at a JS file', function() {
6 | var input = path.join(__dirname, 'data-js/a.js')
7 | var output = path.join(__dirname, 'data-js/a.output.js')
8 | var options
9 | beforeEach(function() {
10 | options = { output: output }
11 | minifier.minify(input, options)
12 | })
13 | afterEach(function() {
14 | fs.unlinkSync(output)
15 | })
16 | it('should output a minified version', function() {
17 | expect(fs.existsSync(output))
18 | .to.be.ok
19 | })
20 | it('should not modify the options object', function() {
21 | expect(options).to.deep.equal({ output: output })
22 | })
23 | })
24 | describe('When aimed at a folder of JS files', function() {
25 | var input = path.join(__dirname, 'data-js')
26 | var options
27 | beforeEach(function() {
28 | options = { template: 'output-{{filename}}.{{ext}}' }
29 | minifier.minify(input, options)
30 | })
31 | afterEach(function() {
32 | options.cleanOnly = true
33 | minifier.minify(input, options)
34 | })
35 | it('should minify both files', function() {
36 | expect(fs.existsSync(path.join(input, 'output-a.js')))
37 | .to.be.true
38 | expect(fs.existsSync(path.join(input, 'output-b.js')))
39 | .to.be.true
40 | })
41 |
42 | describe('and the `clean` option is set', function() {
43 | beforeEach(function() {
44 | fs.renameSync(path.join(input, 'a.js'), path.join(input, 'aa.js'))
45 | options.clean = true
46 | minifier.minify(input, options)
47 | })
48 | afterEach(function() {
49 | fs.renameSync(path.join(input, 'aa.js'), path.join(input, 'a.js'))
50 | })
51 |
52 | describe('and the `skip` option is set', function() {
53 | beforeEach(function() {
54 | options.skip = [ 'b.js' ]
55 | minifier.minify(input, options)
56 | })
57 | it('should skip the right files', function() {
58 | expect(fs.existsSync(path.join(input, 'output-b.js')))
59 | .to.be.false
60 | })
61 | it('should minify the other file', function() {
62 | expect(fs.existsSync(path.join(input, 'output-aa.js')))
63 | .to.be.true
64 | })
65 | })
66 |
67 | it('should remove the old files', function() {
68 | expect(fs.existsSync(path.join(input, 'output-a.js')))
69 | .to.be.false
70 | })
71 | it('should minify both files', function() {
72 | expect(fs.existsSync(path.join(input, 'output-aa.js')))
73 | .to.be.true
74 | expect(fs.existsSync(path.join(input, 'output-b.js')))
75 | .to.be.true
76 | })
77 | })
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/test/integration/api.errors.js:
--------------------------------------------------------------------------------
1 | describe('integration/api.errors.js', function() {
2 | var minifier = require('../../index')
3 | var fs = require('fs')
4 | var path = require('path')
5 |
6 | beforeEach(function() {
7 | fzkes.fake(fs, 'writeFile')
8 | fzkes.fake(fs, 'writeFileSync')
9 | })
10 | afterEach(function() {
11 | fzkes.restore()
12 | })
13 |
14 | describe('When giving input as a non-supported filetype', function() {
15 | var input
16 | beforeEach(function() {
17 | input = path.join(__dirname, 'data/a.html')
18 | })
19 | it('should throw an error', function() {
20 | expect(function() {
21 | minifier.minify(input)
22 | }).to.throw()
23 | })
24 | describe('and listening to `error` event', function() {
25 | var listener
26 | beforeEach(function() {
27 | listener = fzkes.fake('listener')
28 | minifier.on('error', listener)
29 |
30 | minifier.minify(input)
31 | })
32 | afterEach(function() {
33 | minifier.removeListener('error', listener)
34 | })
35 | it('should emit the event', function() {
36 | expect(listener).to.have.been.called
37 | })
38 | it('should not create the files', function() {
39 | expect(fs.writeFile).not.to.have.been.called
40 | expect(fs.writeFileSync).not.to.have.been.called
41 | })
42 | })
43 | })
44 | describe('When giving both `output` and `template` option', function() {
45 | var input
46 | var options
47 | beforeEach(function() {
48 | input = path.join(__dirname, 'data-js/a.js')
49 | options =
50 | { output: 'a.min.js'
51 | , template: 'abc'
52 | }
53 | })
54 | it('should throw', function() {
55 | expect(function() {
56 | minifier.minify(input, options)
57 | }).to.throw()
58 | })
59 | describe('and listening to `error` event', function() {
60 | var listener
61 | beforeEach(function() {
62 | listener = fzkes.fake('listener')
63 | minifier.on('error', listener)
64 |
65 | minifier.minify(input, options)
66 | })
67 | afterEach(function() {
68 | minifier.removeListener('error', listener)
69 | })
70 | it('should emit the event', function() {
71 | expect(listener).to.have.been.called
72 | })
73 | it('should not create the files', function() {
74 | expect(fs.writeFile).not.to.have.been.called
75 | expect(fs.writeFileSync).not.to.have.been.called
76 | })
77 | })
78 | })
79 | describe('When giving `output` option against a directory', function() {
80 | var input
81 | var options
82 | beforeEach(function() {
83 | input = path.join(__dirname, 'data-js')
84 | options = { output: 'a.b' }
85 | })
86 | it('should throw', function() {
87 | expect(function() {
88 | minifier.minify(input, options)
89 | }).to.throw()
90 | })
91 | describe('and listening to `error` event', function() {
92 | var listener
93 | beforeEach(function() {
94 | listener = fzkes.fake('listener')
95 | minifier.on('error', listener)
96 |
97 | minifier.minify(input, options)
98 | })
99 | afterEach(function() {
100 | minifier.removeListener('error', listener)
101 | })
102 | it('should give the error', function() {
103 | expect(listener).to.have.been.called
104 | })
105 | it('should not create the files', function() {
106 | expect(fs.writeFile).not.to.have.been.called
107 | expect(fs.writeFileSync).not.to.have.been.called
108 | })
109 | })
110 | })
111 | })
112 |
--------------------------------------------------------------------------------
/test/unit/minifier.js:
--------------------------------------------------------------------------------
1 | describe('unit/minifier.js', function() {
2 | var minifier = require('../../index')
3 | var fs = require('fs')
4 | var path = require('path')
5 |
6 | beforeEach(function() {
7 | fzkes.fake(fs, 'readFileSync').calls(function(file) {
8 | return 'read ' + file
9 | })
10 | fzkes.fake(fs, 'statSync').returns({ isDirectory: function() { return false } })
11 | fzkes.fake(fs, 'writeFileSync')
12 | })
13 | afterEach(function() {
14 | fzkes.restore()
15 | })
16 |
17 | describe('When having a license block in JS', function() {
18 | var opts
19 | beforeEach(function() {
20 | opts = { output: 'out.js' }
21 | })
22 | describe('as block comment', function() {
23 | beforeEach(function() {
24 | fs.readFileSync.withArgs('a.js')
25 | .returns('/* test a\n * test\n */\n"a";')
26 | })
27 | it('should retain that block', function() {
28 | minifier.minify('a.js', opts)
29 | expect(fs.writeFileSync._calls[0][1])
30 | .to.equal('/* test a\n * test\n */\n"a";')
31 | })
32 | })
33 | describe('as single-line comments', function() {
34 | beforeEach(function() {
35 | fs.readFileSync.withArgs('a.js')
36 | .returns('// test a\n// test\n"a";')
37 | })
38 | it('should retain that block', function() {
39 | minifier.minify('a.js', opts)
40 | expect(fs.writeFileSync._calls[0][1])
41 | .to.equal('// test a\n// test\n"a";')
42 | })
43 | })
44 | describe('with the `noComments` flag', function() {
45 | beforeEach(function() {
46 | opts.noComments = true
47 | })
48 | it('should remove comment blocks', function() {
49 | fs.readFileSync.withArgs('a.js')
50 | .returns('/* test a\n * test\n */\n"a";')
51 | minifier.minify('a.js', opts)
52 | expect(fs.writeFileSync._calls[0][1])
53 | .to.equal('"a";')
54 | })
55 | it('should remove comment lines', function() {
56 | fs.readFileSync.withArgs('a.js')
57 | .returns('// test a\n// test\n"a";')
58 | minifier.minify('a.js', opts)
59 | expect(fs.writeFileSync._calls[0][1])
60 | .to.equal('"a";')
61 | })
62 | })
63 | })
64 |
65 | describe('When having a license block in CSS', function() {
66 | var opts
67 | beforeEach(function() {
68 | opts = { output: 'out.css' }
69 | fs.readFileSync.withArgs('a.css')
70 | .returns('/* test a\n * test\n */\na{color:blue}')
71 | fs.readFileSync.withArgs('b.css')
72 | .returns('/* test b\n * test\n */\nb{color:green}')
73 | fs.readFileSync.withArgs('c.css')
74 | .returns('/* test c\n * test\n */\n@import url(a.css);\n@import url(b.css);')
75 | })
76 | describe('in a single file', function() {
77 | it('should retain that block', function() {
78 | minifier.minify('a.css', opts)
79 | expect(fs.writeFileSync._calls[0][1])
80 | .to.equal('/* test a\n * test\n */\na{color:blue}')
81 | })
82 | })
83 | describe('in referenced files', function() {
84 | it('should retain all blocks', function() {
85 | var result = minifier.minify('c.css', opts)
86 | expect(fs.writeFileSync._calls[0][1])
87 | .to.equal('/* test c\n * test\n */\n/* test a\n * test\n */\na{color:blue}\n/* test b\n * test\n */\nb{color:green}')
88 | })
89 | })
90 | describe('with the `noComments` flag set', function() {
91 | beforeEach(function() {
92 | opts.noComments = true
93 | })
94 | it('should remove the comment block', function() {
95 | minifier.minify('a.css', opts)
96 | expect(fs.writeFileSync._calls[0][1])
97 | .to.equal('a{color:blue}')
98 | })
99 | })
100 | })
101 | })
102 |
--------------------------------------------------------------------------------
/test/unit/utils.js:
--------------------------------------------------------------------------------
1 | describe('unit/utils.js', function() {
2 | var utils = require('../../src/utils')
3 | var minifier = require('../../index')
4 | it('should expose `generateOutputName`', function() {
5 | expect(minifier.generateOutputName).to.equal(utils.generateOutputName)
6 | })
7 | describe('When calling `generateOutputName()`', function() {
8 | describe('with no template vars', function() {
9 | it('should only inject `min`', function() {
10 | expect(utils.generateOutputName('a.js'))
11 | .to.equal('a.min.js')
12 | expect(utils.generateOutputName('a.css'))
13 | .to.equal('a.min.css')
14 | })
15 | })
16 | describe('with template-vars', function() {
17 | it('should place the output next to the input', function() {
18 | var opts = { template: '1.{{ext}}' }
19 | expect(utils.generateOutputName('a/b.js', opts))
20 | .to.equal('a/1.js')
21 | })
22 | it('should replace {{filename}}', function() {
23 | var opts = { template: '1-{{filename}}-2' }
24 | expect(utils.generateOutputName('a.js', opts))
25 | .to.equal('1-a-2')
26 | expect(utils.generateOutputName('a.css', opts))
27 | .to.equal('1-a-2')
28 | })
29 | it('should replace {{ext}}', function() {
30 | var opts = { template: '1.{{ext}}' }
31 | expect(utils.generateOutputName('a.js', opts))
32 | .to.equal('1.js')
33 | expect(utils.generateOutputName('a.css', opts))
34 | .to.equal('1.css')
35 | })
36 | })
37 | describe('with `{{md5}}`', function() {
38 | it('should throw an exception if no content is given', function() {
39 | var opts =
40 | { template: '{{md5}}'
41 | }
42 | expect(function() {
43 | utils.generateOutputName('filename.ext', opts)
44 | }).to.throw(/content.*required/i)
45 | })
46 | it('should support digest content correctly', function() {
47 | var opts =
48 | { content: 'content'
49 | , template: '{{md5}}'
50 | }
51 | expect(utils.generateOutputName('filename.ext', opts))
52 | .to.equal('9a0364b9e99bb480dd25e1f0284c8555')
53 | })
54 | })
55 | describe('with `{{sha}}`', function() {
56 | it('should throw an exception if no content is given', function() {
57 | var opts =
58 | { template: '{{sha}}'
59 | }
60 | expect(function() {
61 | utils.generateOutputName('filename.ext', opts)
62 | }).to.throw(/content.*required/i)
63 | })
64 | it('should support sha-hashed content', function() {
65 | var opts =
66 | { content: 'content'
67 | , template: '{{sha}}'
68 | }
69 | expect(utils.generateOutputName('filename.ext', opts))
70 | .to.equal('ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73')
71 | })
72 | })
73 | describe('with `regex` flag', function() {
74 | it('should return a regex', function() {
75 | var opts = { regex: true }
76 | expect(utils.generateOutputName('a.b', opts))
77 | .to.be.a('RegExp')
78 | })
79 | it('should escape `.` properly', function() {
80 | var opts = { regex: true, template: 'a.min.b' }
81 | expect(utils.generateOutputName('a.b', opts).toString())
82 | .to.equal('/a\\.min\\.b/')
83 | })
84 | it('should return `.*` for `{{sha}}`', function() {
85 | var opts = { regex: true, template: '{{filename}}.{{sha}}.{{ext}}' }
86 | expect(utils.generateOutputName('a.b', opts).toString())
87 | .to.equal('/a\\..*\\.b/')
88 | })
89 | it('should return `.*` for `{{md5}}`', function() {
90 | var opts = { regex: true, template: '{{filename}}.{{md5}}.{{ext}}' }
91 | expect(utils.generateOutputName('a.b', opts).toString())
92 | .to.equal('/a\\..*\\.b/')
93 | })
94 | })
95 | describe('with `glob` flag', function() {
96 | it('should return a string', function() {
97 | var opts = { glob: true }
98 | expect(utils.generateOutputName('a.b', opts))
99 | .to.be.a('string')
100 | })
101 | it('should return `*` for `{{sha}}`', function() {
102 | var opts = { glob: true, template: '{{filename}}.{{sha}}.{{ext}}' }
103 | expect(utils.generateOutputName('a.b', opts))
104 | .to.equal('a.*.b')
105 | })
106 | it('should return `*` for `{{md5}}`', function() {
107 | var opts = { glob: true, template: '{{filename}}.{{md5}}.{{ext}}' }
108 | expect(utils.generateOutputName('a.b', opts))
109 | .to.equal('a.*.b')
110 | })
111 | })
112 | })
113 | })
114 |
--------------------------------------------------------------------------------
/src/minify.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | var path = require('path')
3 | var fmerge = require('fmerge')
4 | var format = require('util').format
5 | var sqwish = require('sqwish')
6 | var uglify = require('uglify-js')
7 | var stripUTF8ByteOrder = require('./utils').stripUTF8ByteOrder
8 | var generateOutput = require('./utils').generateOutputName
9 | var glob = require('glob')
10 | var cssParser = require('css-resolve-import')
11 |
12 | var EventEmitter = require('events').EventEmitter
13 | var obj = new EventEmitter()
14 |
15 | obj.minify = minify
16 | obj.generateOutputName = generateOutput
17 |
18 | module.exports = obj
19 |
20 | function minify(input, options) {
21 | options = fmerge({}, options)
22 |
23 | var output
24 | var template
25 |
26 | if(!input || (Array.isArray(input) && input.length == 0)) {
27 | obj.emit('error', new Error('The input is required'))
28 | }
29 |
30 | if(options.cleanOnly) {
31 | options.clean = true
32 | }
33 | output = options.output
34 | template = options.template
35 |
36 | if(output && template) {
37 | return obj.emit(
38 | 'error'
39 | , new Error('It does not make sense to provide both --output and '
40 | + '--template options. Please choose one.')
41 | )
42 | }
43 |
44 | if(!Array.isArray(input) && fs.statSync(input).isDirectory()) {
45 | if(output) {
46 | return obj.emit('error',
47 | new Error('You cannot use `output` option against a directory'))
48 | }
49 | if(options.clean) {
50 | clean(input, template || '{{filename}}.min.{{ext}}')
51 | }
52 | if(options.cleanOnly) {
53 | return
54 | }
55 |
56 | var files = glob.sync(path.join(input, '**/*.js'))
57 | .concat(
58 | glob.sync(path.join(input, '**/*.css'))
59 | )
60 | if(options.skip) {
61 | files = files.filter(function(file) {
62 | return !options.skip.some(function(filter) {
63 | return ~file.indexOf(filter)
64 | })
65 | })
66 | }
67 | files.every(x => handleInputs([x]))
68 |
69 | return
70 | }
71 |
72 | var inputs = Array.isArray(input) ? input : [input]
73 |
74 | if(options.clean) {
75 | if(template) {
76 | clean(path.dirname(inputs[0]), template)
77 | } else if(fs.existsSync(output)) {
78 | fs.unlinkSync(output)
79 | }
80 | }
81 |
82 | if(options.cleanOnly) {
83 | return
84 | }
85 |
86 | handleInputs(inputs)
87 |
88 | function handleInputs(inputs) {
89 | var extensionRegex = /(\.js|css)$/
90 |
91 | var usedExtensions = inputs
92 | .map(function(i) { return i.match(extensionRegex) })
93 | .filter(function(i) { return i != null })
94 | .map(function(i) { return i[1] })
95 | .filter(function(ext, idx, arr) { return arr.indexOf(ext) == idx })
96 |
97 | if(usedExtensions.length > 1) {
98 | obj.emit('error', new Error('Please only use one type of extension per run'))
99 | return false
100 | } else if(usedExtensions.length == 0 || usedExtensions[0].match(extensionRegex) == null) {
101 | obj.emit('error', new Error('Please reference files with the extension as either .js or .css'))
102 | return false
103 | }
104 |
105 | var jsFiles = inputs.filter(x => x.endsWith('.js'))
106 | var cssFiles = inputs.filter(x => x.endsWith('.css'))
107 | if(jsFiles.length > 0) {
108 | js(jsFiles)
109 | }
110 | if(cssFiles.length > 0) {
111 | css(cssFiles)
112 | }
113 | return true
114 | }
115 |
116 | function js(inputs) {
117 | var max = inputs.map(function(input) {
118 | return stripUTF8ByteOrder(fs.readFileSync(input, 'utf8'))
119 | }).join(';\n')
120 |
121 | var comment = firstComment(max)
122 | var min = uglify.minify(max, fmerge(options.uglify, { fromString: true })).code
123 | var opts = { content: min, template: template }
124 | var renderedOutput = output || generateOutput(inputs[0], opts)
125 |
126 | if(comment) {
127 | min = comment +'\n' + min
128 | }
129 |
130 | fs.writeFileSync(renderedOutput, min)
131 | }
132 |
133 | function css(inputs) {
134 | var inDir = path.dirname(inputs[0])
135 | var outDir = path.dirname(output || inputs[0])
136 | var root = path.join(inDir, path.relative(inDir, outDir))
137 | var min = inputs.map(input => cssParser(input, root, function(max) {
138 | var max = stripUTF8ByteOrder(max)
139 | var comment = firstComment(max)
140 | var min = sqwish.minify(max, false)
141 |
142 | if(comment) {
143 | min = comment + '\n' + min
144 | }
145 |
146 | return min
147 | })).join('\n')
148 | var opts = { content: min, template: template }
149 | var renderedOutput = output || generateOutput(inputs[0], opts)
150 |
151 | fs.writeFileSync(renderedOutput, min)
152 | }
153 |
154 | function clean(dir, template) {
155 | template = template.replace(/{{[^}]*}}/g, '*')
156 | glob.sync(path.join(dir, '**', template)).forEach(function(file) {
157 | fs.unlinkSync(file)
158 | })
159 | }
160 |
161 | function firstComment(content) {
162 | if(options.noComments) return null
163 | content = content.trim()
164 | if(content[0] == '/' && content[1] == '*') {
165 | return content.substring(0, content.indexOf('*/')+2)
166 | }
167 | if(content[0] == '/' && content[1] == '/') {
168 | var lines = content.split(/[\r\n]{1,2}/g)
169 | content = lines[0]
170 | for(var i = 1; i < lines.length; i++) {
171 | var line = lines[i]
172 | if(line[0] == '/' && line[1] == '/') {
173 | content += '\n' + line
174 | }
175 | }
176 | return content
177 | }
178 | return null
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/test/unit/css.parser.js:
--------------------------------------------------------------------------------
1 | describe('unit/css.parser.js', function() {
2 | var parser = require('../../src/css')
3 | var fs = require('fs')
4 | var path = require('path')
5 |
6 | beforeEach(function() {
7 | fzkes.fake(fs, 'readFileSync').calls(function(file) {
8 | return 'read ' + file
9 | })
10 | })
11 | afterEach(function() {
12 | fzkes.restore()
13 | })
14 |
15 | describe('When providing a data-url', function() {
16 | it('should not replace forward slashes', function() {
17 | fs.readFileSync.withArgs('a')
18 | .returns('a{background:url(data:image/png;base64,abc//def)}')
19 | var result = parser.parse('a')
20 | expect(result).to.equal('a{background:url(data:image/png;base64,abc//def)}')
21 | })
22 | })
23 |
24 | describe('When providing a root path', function() {
25 | it('should fix urls according to the base path', function() {
26 | fs.readFileSync.withArgs('a/b').returns('a{background: url(c);}')
27 | var result = parser.parse('a/b', 'a')
28 | expect(result).to.equal('a{background: url(c);}')
29 | })
30 | it('should fix imported urls according to the base path', function() {
31 | fs.readFileSync.withArgs('a/b').returns('@import url(c);')
32 | fs.readFileSync.withArgs(path.join('a/c')).returns('a{background: url(d);}')
33 | var result = parser.parse('a/b', 'a')
34 | expect(result).to.equal('a{background: url(d);}')
35 | })
36 | it('should work with absolute base paths', function() {
37 | fs.readFileSync.withArgs('/a/file').returns('@import url(b/file);')
38 | fs.readFileSync.withArgs(path.join('/a/b/file')).returns('a{background: url(c/file);}')
39 | var result = parser.parse('/a/file', '/a')
40 | expect(result).to.equal('a{background: url(b/c/file);}')
41 | })
42 | it('should work with deeper nested base paths', function() {
43 | fs.readFileSync.withArgs('/a/b/file').returns('@import url(c/file);')
44 | fs.readFileSync.withArgs(path.join('/a/b/c/file')).returns('a{background: url(d/file);}')
45 | var result = parser.parse('/a/b/file', '/a')
46 | expect(result).to.equal('a{background: url(b/c/d/file);}')
47 | })
48 | it('should not change absolute import urls', function() {
49 | fs.readFileSync.withArgs('/a/b').returns('@import url(/absolute/path);')
50 | parser.parse('/a/b', '/a')
51 | expect(fs.readFileSync).to.have.been.calledWith('/absolute/path')
52 | })
53 | it('should not change other absolute urls', function() {
54 | fs.readFileSync.withArgs('/a/b').returns('a{background:url(/absolute/path);}')
55 | var result = parser.parse('/a/b', '/a')
56 | expect(result).to.equal('a{background:url(/absolute/path);}')
57 | })
58 | })
59 | describe('When parsing an import', function() {
60 | it('should take the current path into consideration', function() {
61 | fs.readFileSync.withArgs('a/b').returns('@import url(c);')
62 | parser.parse('a/b')
63 | expect(fs.readFileSync).to.have.been.calledWith(path.join('a/c'))
64 | })
65 | it('should apply the path to any other urls as well', function() {
66 | fs.readFileSync.withArgs('a/b').returns('a{background: url(c);}')
67 | var result = parser.parse('a/b')
68 | expect(result).to.equal('a{background: url(a/c);}')
69 | })
70 | it('should apply the path through imports', function() {
71 | fs.readFileSync.withArgs('a').returns('@import url(b/c);')
72 | fs.readFileSync.withArgs(path.join('b/c')).returns('a{background:url(d);')
73 | var result = parser.parse('a')
74 | expect(result).to.equal('a{background:url(b/d);')
75 | })
76 | })
77 | describe('When parsing a file with different imports', function() {
78 | var result
79 | it('should work with urls containing white-space', function() {
80 | fs.readFileSync.withArgs('a.css').returns('@import url( file ) ;')
81 | result = parser.parse('a.css')
82 | expect(result).to.equal('read file')
83 | })
84 | it('should work with strings with single quotes', function() {
85 | fs.readFileSync.withArgs('a.css').returns("@import 'file';")
86 | result = parser.parse('a.css')
87 | expect(result).to.equal('read file')
88 | })
89 | it('should work with strings with double quotes', function() {
90 | fs.readFileSync.withArgs('a.css').returns('@import "file";')
91 | result = parser.parse('a.css')
92 | expect(result).to.equal('read file')
93 | })
94 | it('should work with urls without quotes', function() {
95 | fs.readFileSync.withArgs('a.css').returns('@import url(file);')
96 | result = parser.parse('a.css')
97 | expect(result).to.equal('read file')
98 | })
99 | it('should work with urls with single quotes', function() {
100 | fs.readFileSync.withArgs('a.css').returns("@import url('file');")
101 | result = parser.parse('a.css')
102 | expect(result).to.equal('read file')
103 | })
104 | it('should work with urls with double quotes', function() {
105 | fs.readFileSync.withArgs('a.css').returns('@import url("file");')
106 | result = parser.parse('a.css')
107 | expect(result).to.equal('read file')
108 | })
109 | })
110 | describe('When calling parse() with a css file with no imports', function() {
111 | var result
112 | beforeEach(function() {
113 | fs.readFileSync.withArgs('a.css').returns('abc');
114 | result = parser.parse('a.css')
115 | })
116 | it('should ask the file-system for the file', function() {
117 | expect(fs.readFileSync).to.have.been.calledWith('a.css', 'utf8')
118 | })
119 | it('should return the file', function() {
120 | expect(result).to.equal('abc')
121 | })
122 | })
123 | })
124 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | This project should be considered deprecated
2 | --------------------------------------------
3 |
4 | This should be considered deprecated. I simply have neither the time or interest
5 | in keeping it up-to-date.
6 |
7 | For alternatives, consider [less](http://lesscss.org) or [sass](http://sass-lang.com/guide)
8 | for the CSS part. I prefer less because it is more lightweight, but both are good
9 | options and serve much of the same purpose.
10 |
11 | For JS, you can either use [uglify](https://github.com/mishoo/UglifyJS2) directly
12 | (this project is already just a thin wrapper around uglify). Alternatively, look
13 | to bundlers like [webpack](https://webpack.js.org), but there are tons of
14 | different ones.
15 |
16 | ---
17 |
18 | minifier
19 | ========
20 |
21 | A simple tool for minifying CSS/JS without a big setup.
22 |
23 | Feature highlights
24 | ------------------
25 |
26 | - It minifies JS and CSS
27 | - URLs in CSS are updated relative to the output location
28 | - It automatically resolves `@import` statements in CSS.
29 |
30 |
31 | How to install
32 | --------------
33 |
34 | There are two ways to install it:
35 |
36 | 1. `npm install [-g] minifier`
37 | 2. Cloning directly from [github](https://github.com/fizker/minifier).
38 |
39 | Installing through `npm` will create a binary (`minify`) in the usual
40 | locations. Cloning and installing from `github` will not, but the `index.js`
41 | file can be executed either directly or via `node index.js`; this is the file
42 | that the binary links to anyway.
43 |
44 |
45 | How to run from a command-line
46 | ------------------------------
47 |
48 | Running it is simple:
49 |
50 | minify [--output path/to/put/file] path/to/file
51 |
52 | If the output parameter is not set, it will place a file next to the original,
53 | with the suffix `.min`.
54 |
55 | For example, `minifier script.js` will output `script.min.js`, whereas
56 | `minifier --output out.js script.js` will output `out.js`.
57 |
58 | A folder can also be targeted. When that is done, it will minify all css and js
59 | file in that folder.
60 |
61 | In that case, `--output` does not make much sense, as all files will be minified
62 | to the same. If the name still requires a specific pattern such as a timestamp,
63 | `--template` is the option for you. Note that files named after a template will
64 | be left in the same folder as the original file.
65 |
66 | There are various options available:
67 |
68 | - `{{filename}}` is the original filename.
69 | - `{{ext}}` is the extension.
70 | - `{{sha}}` is a sha-digest of the unminified file contents.
71 | - `{{md5}}` is a md5-digest of the unminified file contents.
72 |
73 | For example, `{{filename}}-{{md5}}.min.{{ext}}` will make `abc.js` into something
74 | like `abc-f90731d65c61af25b149658a120d26cf.min.js`.
75 |
76 | To avoid the minification of previously minified files, there is a `--clean`
77 | option, which will delete all files that match the output pattern.
78 |
79 | This also means that any real files that match the pattern will be removed as
80 | well, so please be careful.
81 |
82 |
83 | Running from a node-script
84 | --------------------------
85 |
86 | It is also possible to run the minifier from within another node script:
87 |
88 | var minifier = require('minifier')
89 | var input = '/some/path'
90 |
91 | minifier.on('error', function(err) {
92 | // handle any potential error
93 | })
94 | minifier.minify(input, options)
95 |
96 | As with the command-line variant, the input should be a path to either a
97 | javascript file, a css file or a folder.
98 |
99 | The options-dictionary takes the same parameters as the command-line variant:
100 |
101 | - output: A path-string that tells where to put the output.
102 | - template: A string template for how to build the outputted filenames.
103 | - clean: A bool for whether other files with names similar to the template
104 | should be deleted before minifying the contents of a folder.
105 | - cleanOnly: A bool for whether to run with `clean` option and then exiting
106 | before minifying any files.
107 |
108 | There are one important additional option: `uglify`. This will be passed on to
109 | uglify, so that the minification can be controlled. See the
110 | [uglify documentation](https://github.com/mishoo/UglifyJS2#the-simple-way)
111 | for more details (the `uglify.minify(path, opts)` function is used internally).
112 |
113 | -----
114 |
115 | The method for building the output name from the template is exposed for
116 | convenience:
117 |
118 | var minifier = require('minifier')
119 | var file = 'abc.js'
120 | var template = '{{filename}}.{{md5}}.{{ext}}'
121 | var content = null; // or the content, if md5 or sha1 should be calculated
122 | var result = minifier.generateOutputName(file, { template: template, content: content })
123 |
124 | If the input-path includes any folders, they will also be added to the output.
125 |
126 | If `content` is eschewed, the `md5` and `sha` digests cannot be calculated.
127 |
128 | But there is an option for turning them into either `RegExp` or `glob` compatible
129 | syntax: Simply add `glob: true` or `regex: true` to the options array:
130 |
131 | var result = minifier.generateOutputName(file, { template: template, glob: true })
132 |
133 | `glob` will return a string for passing to a `glob` function, whereas `regex`
134 | will return a `RegExp` instance for manual comparison.
135 |
136 |
137 | Concatenating multiple javascript files
138 | ---------------------------------------
139 |
140 | It is possible to concatenate multiple javascript files into a single, minified file.
141 |
142 | Simply pass multiple in via the CLI interface, or pass an array to the API.
143 |
144 | They will have the same order as the input-parameter.
145 |
146 | CSS URL Rewrites
147 | ----------------
148 |
149 | Any URLs that are found in CSS files, are automatically updated relative to the output destination. An example is shown below:
150 |
151 | ```
152 | - styles.css
153 | - dist/
154 | - styles.min.css
155 | - lib/
156 | - backgrond.jpg
157 |
158 |
159 | styles.css
160 |
161 | .background {
162 | background-image: url("lib/background.jpg");
163 | }
164 |
165 | styles.min.css
166 |
167 | .background {
168 | background-image: url("../lib/background.jpg");
169 | }
170 | ```
171 |
172 | Running the tests
173 | -----------------
174 |
175 | After installing from [github](https://github.com/fizker/minifier), simply run
176 | `npm test`.
177 |
178 | There is a script called `prepareManualTests.js`, which will run the script
179 | against the css-files inside `test/manual/css/` and provides a real-world
180 | example of the CSS minification tools.
181 |
182 | The manual tests can be seen by opening `test/manual/index.html` in a browser
183 | after executing `prepareManualTests.js`.
184 |
185 |
186 | Credits
187 | -------
188 |
189 | In no particular order:
190 |
191 | - [duckduckgo](http://duckduckgo.com) for the image used by the manual tests.
192 | - [sqwish](https://github.com/ded/sqwish) for minifying CSS files.
193 | - [uglify-js](https://github.com/mishoo/UglifyJS2) for minifying JS files.
194 | - [commander](https://github.com/visionmedia/commander.js) for command-line
195 | interaction.
196 | - [mocha](https://github.com/visionmedia/mocha) and [chai](http://chaijs.com)
197 | for testing home-brewed logic.
198 | - [glob](https://github.com/isaacs/node-glob) for trawling through the
199 | file-system when targetting a folder.
200 | - [hogan.js](http://twitter.github.com/hogan.js/) for parsing the template
201 | string.
202 |
--------------------------------------------------------------------------------
/test/integration/api.css.js:
--------------------------------------------------------------------------------
1 | describe('integration/api.css.js', function() {
2 | var minifier = require('../../index')
3 | var fs = require('fs')
4 | var path = require('path')
5 |
6 | describe('When calling the api on a folder', function() {
7 | const input = path.join(__dirname, 'data')
8 | const template = '{{filename}}.{{md5}}.out.{{ext}}'
9 |
10 | beforeEach(function() {
11 | minifier.minify(input, { template: template })
12 | })
13 | afterEach(function() {
14 | safeDelete(path.join(__dirname, 'data/a.5fe39ce3416b224850714849c52781ee.out.css'))
15 | safeDelete(path.join(__dirname, 'data/b.463682a6278fbd2d2c7c3691a4f5b441.out.css'))
16 | safeDelete(path.join(__dirname, 'data/a/c.ef9596f4e227fdc4d4131f2e9b278e00.out.css'))
17 | safeDelete(path.join(__dirname, 'data/b/d.a8c574a0b15ea78c32e0ea5db202ad21.out.css'))
18 | })
19 |
20 | it('should minify all the files', function() {
21 | expect(fs.existsSync(path.join(__dirname, 'data/a.5fe39ce3416b224850714849c52781ee.out.css')))
22 | .to.be.true
23 | expect(fs.existsSync(path.join(__dirname, 'data/b.463682a6278fbd2d2c7c3691a4f5b441.out.css')))
24 | .to.be.true
25 | expect(fs.existsSync(path.join(__dirname, 'data/a/c.ef9596f4e227fdc4d4131f2e9b278e00.out.css')))
26 | .to.be.true
27 | expect(fs.existsSync(path.join(__dirname, 'data/b/d.a8c574a0b15ea78c32e0ea5db202ad21.out.css')))
28 | .to.be.true
29 | })
30 | describe('with the `clean` option set', function() {
31 | var oldContent
32 | beforeEach(function() {
33 | oldContent = fs.readFileSync(path.join(input, 'a.css'))
34 | fs.writeFileSync(path.join(__dirname, 'data/a.css'), 'abc{}')
35 | minifier.minify(input, { template: template, clean: true })
36 | })
37 | afterEach(function() {
38 | fs.writeFileSync(path.join(__dirname, 'data/a.css'), oldContent)
39 | safeDelete(path.join(__dirname, 'data/a.34c67064c3f76ca1f5798ad0fd1f8f98.out.css'))
40 | })
41 | it('should delete the old file', function() {
42 | expect(fs.existsSync(path.join(__dirname, 'data/a.5fe39ce3416b224850714849c52781ee.out.css')))
43 | .to.be.false
44 | expect(fs.existsSync(path.join(__dirname, 'data/b.463682a6278fbd2d2c7c3691a4f5b441.out.css')))
45 | .to.be.true
46 | expect(fs.existsSync(path.join(__dirname, 'data/a/c.ef9596f4e227fdc4d4131f2e9b278e00.out.css')))
47 | .to.be.true
48 | expect(fs.existsSync(path.join(__dirname, 'data/b/d.a8c574a0b15ea78c32e0ea5db202ad21.out.css')))
49 | .to.be.true
50 | })
51 | it('should create the new file correctly', function() {
52 | expect(fs.existsSync(path.join(__dirname, 'data/a.34c67064c3f76ca1f5798ad0fd1f8f98.out.css')))
53 | .to.be.true
54 | })
55 | })
56 | describe('with the `cleanOnly` option set', function() {
57 | beforeEach(function() {
58 | minifier.minify(input, { template: template, cleanOnly: true })
59 | })
60 | it('should clean but not create the files', function() {
61 | expect(fs.existsSync(path.join(__dirname, 'data/a.5fe39ce3416b224850714849c52781ee.out.css')))
62 | .to.be.false
63 | expect(fs.existsSync(path.join(__dirname, 'data/b.463682a6278fbd2d2c7c3691a4f5b441.out.css')))
64 | .to.be.false
65 | expect(fs.existsSync(path.join(__dirname, 'data/a/c.ef9596f4e227fdc4d4131f2e9b278e00.out.css')))
66 | .to.be.false
67 | expect(fs.existsSync(path.join(__dirname, 'data/b/d.a8c574a0b15ea78c32e0ea5db202ad21.out.css')))
68 | .to.be.false
69 | })
70 | })
71 | })
72 | describe('When calling the api on a single file', function() {
73 | const input = path.join(__dirname, 'data/a.css')
74 | const output = path.join(__dirname, 'a.output.css')
75 | beforeEach(function() {
76 | minifier.minify(input, { output: output })
77 | })
78 | afterEach(function() {
79 | safeDelete(output)
80 | })
81 |
82 | it('should create the minified file', function() {
83 | expect(fs.existsSync(output)).to.be.ok
84 | })
85 | it('should reformat the url correctly', function() {
86 | var contents = fs.readFileSync(output, 'utf8')
87 | expect(contents).to.match(/\.nested-img[^}]+gfx\/img\.png/)
88 | })
89 | it('should import all files', function() {
90 | var contents = fs.readFileSync(output, 'utf8')
91 | expect(contents)
92 | .to.contain('.a-file')
93 | .and.to.contain('.b-file')
94 | .and.to.contain('.c-file')
95 | .and.to.contain('.d-file')
96 | })
97 | describe('with the `cleanOnly` option set', function() {
98 | beforeEach(function() {
99 | minifier.minify(input, { output: output, cleanOnly: true })
100 | })
101 | it('should remove the file', function() {
102 | expect(fs.existsSync(output)).to.be.false
103 | })
104 | })
105 | describe('with the `template` option set', function() {
106 | const template = 'template.{{md5}}.out.{{ext}}'
107 | const firstOutput = path.join(__dirname, 'data/template.5fe39ce3416b224850714849c52781ee.out.css')
108 | beforeEach(function() {
109 | minifier.minify(input, { template: template })
110 | })
111 | afterEach(function() {
112 | safeDelete(firstOutput)
113 | })
114 | it('should create the file correctly', function() {
115 | expect(fs.existsSync(firstOutput))
116 | .to.be.true
117 | })
118 | describe('and the `clean` option', function() {
119 | var oldContent
120 | const secondOutput = path.join(__dirname, 'data/template.34c67064c3f76ca1f5798ad0fd1f8f98.out.css')
121 | beforeEach(function() {
122 | oldContent = fs.readFileSync(input)
123 | fs.writeFileSync(input, 'abc{}')
124 | minifier.minify(input, { template: template, clean: true })
125 | })
126 | afterEach(function() {
127 | fs.writeFileSync(input, oldContent)
128 | safeDelete(secondOutput)
129 | })
130 | it('should delete the old file', function() {
131 | expect(fs.existsSync(firstOutput))
132 | .to.be.false
133 | })
134 | it('should create the new file', function() {
135 | expect(fs.existsSync(secondOutput))
136 | .to.be.true
137 | })
138 | })
139 | describe('and the `cleanOnly` option', function() {
140 | beforeEach(function() {
141 | minifier.minify(input, { template: template, cleanOnly: true })
142 | })
143 | it('should clean, but not create the file', function() {
144 | expect(fs.existsSync(firstOutput))
145 | .to.be.false
146 | })
147 | })
148 | })
149 | })
150 | describe('When calling the api on a list of files', function() {
151 | const inputs = ['a/c', 'b'].map(x => path.join(__dirname, `data/${x}.css`))
152 | const output = path.join(__dirname, 'output.css')
153 | beforeEach(function() {
154 | minifier.minify(inputs, { output: output })
155 | })
156 | afterEach(function() {
157 | safeDelete(output)
158 | })
159 |
160 | it('should create the minified file', function() {
161 | expect(fs.existsSync(output)).to.be.ok
162 | })
163 | it('should reformat the url correctly', function() {
164 | var contents = fs.readFileSync(output, 'utf8')
165 | expect(contents).to.match(/\.nested-img[^}]+gfx\/img\.png/)
166 | })
167 | it('should import all files', function() {
168 | var contents = fs.readFileSync(output, 'utf8')
169 | expect(contents)
170 | .to.contain('.b-file')
171 | .and.to.contain('.c-file')
172 | .and.to.contain('.d-file')
173 | })
174 | describe('with the `cleanOnly` option set', function() {
175 | beforeEach(function() {
176 | minifier.minify(inputs, { output: output, cleanOnly: true })
177 | })
178 | it('should remove the file', function() {
179 | expect(fs.existsSync(output)).to.be.false
180 | })
181 | })
182 | describe('with the `template` option set', function() {
183 | const template = 'template.{{md5}}.out.{{ext}}'
184 | const firstOutput = path.join(__dirname, 'data/a/template.1909ce21348346f98abc37cbeae8e805.out.css')
185 | beforeEach(function() {
186 | minifier.minify(inputs, { template: template })
187 | })
188 | afterEach(function() {
189 | safeDelete(firstOutput)
190 | })
191 | it('should create the file correctly', function() {
192 | expect(fs.existsSync(firstOutput))
193 | .to.be.true
194 | })
195 | describe('and the `clean` option', function() {
196 | var oldContent
197 | const secondOutput = path.join(__dirname, 'data/a/template.09c2d80354dcf526e64518d1bd60560b.out.css')
198 | beforeEach(function() {
199 | oldContent = fs.readFileSync(inputs[0])
200 | fs.writeFileSync(inputs[0], 'abc{}')
201 | minifier.minify(inputs, { template: template, clean: true })
202 | })
203 | afterEach(function() {
204 | fs.writeFileSync(inputs[0], oldContent)
205 | safeDelete(secondOutput)
206 | })
207 | it('should delete the old file', function() {
208 | expect(fs.existsSync(firstOutput))
209 | .to.be.false
210 | })
211 | it('should create the new file', function() {
212 | expect(fs.existsSync(secondOutput))
213 | .to.be.true
214 | })
215 | })
216 | describe('and the `cleanOnly` option', function() {
217 | beforeEach(function() {
218 | minifier.minify(inputs, { template: template, cleanOnly: true })
219 | })
220 | it('should clean, but not create the file', function() {
221 | expect(fs.existsSync(firstOutput))
222 | .to.be.false
223 | })
224 | })
225 | })
226 | })
227 |
228 | function safeDelete(file) {
229 | fs.existsSync(file) && fs.unlinkSync(file)
230 | }
231 | })
232 |
--------------------------------------------------------------------------------