├── cjs ├── package.json ├── compressed.js ├── html-minifier.js ├── preview.js ├── svg.js ├── gif.js ├── css.js ├── json.js ├── copy.js ├── html.js ├── xml.js ├── png.js ├── headers.js ├── jpg.js ├── compress.js ├── md.js ├── index.js └── js.js ├── test ├── package.json ├── source │ ├── index.css │ ├── heresy.png │ ├── rpi2.png │ ├── favicon.ico │ ├── archibold.jpg │ ├── index.mjs │ ├── wiki-world.gif │ ├── fa-regular-400.woff2 │ ├── test.md │ ├── index.xml │ ├── index.js │ ├── index.html │ ├── package.json │ ├── text.txt │ └── benja-dark.svg ├── ucompress.jpg ├── prepared │ ├── text.txt │ └── recursive │ │ └── text.txt └── index.js ├── .npmignore ├── .gitignore ├── .travis.yml ├── esm ├── compressed.js ├── html-minifier.js ├── preview.js ├── css.js ├── gif.js ├── svg.js ├── copy.js ├── json.js ├── html.js ├── xml.js ├── png.js ├── headers.js ├── jpg.js ├── compress.js ├── md.js ├── index.js └── js.js ├── LICENSE ├── package.json ├── binary.cjs └── README.md /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/source/index.css: -------------------------------------------------------------------------------- 1 | /* comment */ 2 | body { 3 | font-family: sans-serif; 4 | } 5 | -------------------------------------------------------------------------------- /test/ucompress.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucompress/HEAD/test/ucompress.jpg -------------------------------------------------------------------------------- /test/source/heresy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucompress/HEAD/test/source/heresy.png -------------------------------------------------------------------------------- /test/source/rpi2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucompress/HEAD/test/source/rpi2.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | node_modules/ 3 | rollup/ 4 | test/ 5 | package-lock.json 6 | .travis.yml 7 | -------------------------------------------------------------------------------- /test/source/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucompress/HEAD/test/source/favicon.ico -------------------------------------------------------------------------------- /test/source/archibold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucompress/HEAD/test/source/archibold.jpg -------------------------------------------------------------------------------- /test/source/index.mjs: -------------------------------------------------------------------------------- 1 | import umap from "umap"; 2 | 3 | function test() { 4 | console.log('test'); 5 | } 6 | -------------------------------------------------------------------------------- /test/source/wiki-world.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucompress/HEAD/test/source/wiki-world.gif -------------------------------------------------------------------------------- /test/source/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucompress/HEAD/test/source/fa-regular-400.woff2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | node_modules/ 3 | test/dest/ 4 | test/prepared/text.txt.* 5 | test/prepared/recursive/text.txt.* 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /test/source/test.md: -------------------------------------------------------------------------------- 1 | # this is some markdown 2 | 3 | and this is its content. 4 | 5 | ```js 6 | with (someCode) { 7 | i++; 8 | } 9 | ``` 10 | -------------------------------------------------------------------------------- /test/source/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | git: 5 | depth: 1 6 | branches: 7 | only: 8 | - master 9 | after_success: 10 | - "npm run coveralls" 11 | -------------------------------------------------------------------------------- /esm/compressed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A list of all extensions that will be compressed via brotli and others. 3 | * Every other file will simply be served as is (likely binary). 4 | */ 5 | export default new Set; 6 | -------------------------------------------------------------------------------- /esm/html-minifier.js: -------------------------------------------------------------------------------- 1 | export default { 2 | collapseWhitespace: true, 3 | preserveLineBreaks: true, 4 | html5: true, 5 | keepClosingSlash: true, 6 | removeAttributeQuotes: true, 7 | removeComments: true 8 | }; 9 | -------------------------------------------------------------------------------- /cjs/compressed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * A list of all extensions that will be compressed via brotli and others. 4 | * Every other file will simply be served as is (likely binary). 5 | */ 6 | module.exports = new Set; 7 | -------------------------------------------------------------------------------- /test/source/index.js: -------------------------------------------------------------------------------- 1 | import "./index.mjs"; 2 | 3 | import "non-existent"; 4 | 5 | import("uhtml").then(({render, html}) => { 6 | render(document.body, html` 7 |
8 | Hello World! 9 |
10 | `); 11 | }); 12 | -------------------------------------------------------------------------------- /cjs/html-minifier.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | collapseWhitespace: true, 4 | preserveLineBreaks: true, 5 | html5: true, 6 | keepClosingSlash: true, 7 | removeAttributeQuotes: true, 8 | removeComments: true 9 | }; 10 | -------------------------------------------------------------------------------- /test/source/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ucompress 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/source/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ucompress", 3 | "version": "0.14.7", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/WebReflection/ucompress.git" 7 | }, 8 | "bugs": { 9 | "url": "https://github.com/WebReflection/ucompress/issues" 10 | }, 11 | "homepage": "https://github.com/WebReflection/ucompress#readme" 12 | } 13 | -------------------------------------------------------------------------------- /esm/preview.js: -------------------------------------------------------------------------------- 1 | import {extname} from 'path'; 2 | 3 | import sharp from 'sharp'; 4 | 5 | export default source => { 6 | const dest = source.replace( 7 | new RegExp(`(\\${extname(source)})$`), 8 | '.preview$1' 9 | ); 10 | return sharp(source) 11 | .blur(0x32) 12 | .modulate({ 13 | brightness: 0.6, 14 | saturation: 0.9 15 | }) 16 | .toFile(dest) 17 | .then(() => dest) 18 | ; 19 | }; 20 | -------------------------------------------------------------------------------- /cjs/preview.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {extname} = require('path'); 3 | 4 | const sharp = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('sharp')); 5 | 6 | module.exports = source => { 7 | const dest = source.replace( 8 | new RegExp(`(\\${extname(source)})$`), 9 | '.preview$1' 10 | ); 11 | return sharp(source) 12 | .blur(0x32) 13 | .modulate({ 14 | brightness: 0.6, 15 | saturation: 0.9 16 | }) 17 | .toFile(dest) 18 | .then(() => dest) 19 | ; 20 | }; 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /esm/css.js: -------------------------------------------------------------------------------- 1 | import {readFile, writeFile} from 'fs'; 2 | 3 | import csso from 'csso'; 4 | 5 | import compressed from './compressed.js'; 6 | import compress from './compress.js'; 7 | 8 | compressed.add('.css'); 9 | 10 | /** 11 | * Create a file after minifying via `csso`. 12 | * @param {string} source The source CSS file to minify. 13 | * @param {string} dest The minified destination file. 14 | * @param {Options} [options] Options to deal with extra computation. 15 | * @return {Promise} A promise that resolves with the destination file. 16 | */ 17 | export default (source, dest, options = {}) => new Promise((res, rej) => { 18 | readFile(source, (err, data) => { 19 | if (err) 20 | rej(err); 21 | else { 22 | /* istanbul ignore next */ 23 | const {css} = options.noMinify ? 24 | {css: data} : 25 | csso.minify(data); 26 | // csso apparently has no way to detect errors 27 | writeFile(dest, css, err => { 28 | if (err) 29 | rej(err); 30 | else if (options.createFiles) 31 | compress(source, dest, 'text', options) 32 | .then(() => res(dest), rej); 33 | else 34 | res(dest); 35 | }); 36 | } 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /esm/gif.js: -------------------------------------------------------------------------------- 1 | import {execFile} from 'child_process'; 2 | import {copyFile} from 'fs'; 3 | 4 | import umeta from 'umeta'; 5 | 6 | import headers from './headers.js'; 7 | 8 | const {require: cjs} = umeta(import.meta); 9 | 10 | let gifsicle = ''; 11 | try { 12 | gifsicle = cjs('gifsicle'); 13 | } 14 | catch(meh) { 15 | // gifsicle should be installed via npm 16 | } 17 | 18 | /** 19 | * Create a file after optimizing via `gifsicle`. 20 | * @param {string} source The source GIF file to optimize. 21 | * @param {string} dest The optimized destination file. 22 | * @param {Options} [options] Options to deal with extra computation. 23 | * @return {Promise} A promise that resolves with the destination file. 24 | */ 25 | export default (source, dest, /* istanbul ignore next */ options = {}) => 26 | new Promise((res, rej) => { 27 | const callback = err => { 28 | if (err) 29 | rej(err); 30 | else if (options.createFiles) 31 | headers(source, dest, options.headers) 32 | .then(() => res(dest), rej); 33 | else 34 | res(dest); 35 | }; 36 | /* istanbul ignore else */ 37 | if (gifsicle) 38 | execFile(gifsicle, ['-o', dest, source], callback); 39 | else 40 | copyFile(source, dest, callback); 41 | }); 42 | -------------------------------------------------------------------------------- /esm/svg.js: -------------------------------------------------------------------------------- 1 | import {readFile, writeFile} from 'fs'; 2 | 3 | import {optimize} from 'svgo'; 4 | 5 | import compressed from './compressed.js'; 6 | import compress from './compress.js'; 7 | 8 | compressed.add('.svg'); 9 | 10 | /** 11 | * Create a file after minifying it via `svgo`. 12 | * @param {string} source The source SVG file to minify. 13 | * @param {string} dest The minified destination file. 14 | * @param {Options} [options] Options to deal with extra computation. 15 | * @return {Promise} A promise that resolves with the destination file. 16 | */ 17 | export default (source, dest, options = {}) => 18 | new Promise((res, rej) => { 19 | readFile(source, (err, file) => { 20 | if (err) 21 | rej(err); 22 | else { 23 | const onSVGO = ({data}) => { 24 | writeFile(dest, data, err => { 25 | if (err) 26 | rej(err); 27 | else if (options.createFiles) 28 | compress(source, dest, 'text', options) 29 | .then(() => res(dest), rej); 30 | else 31 | res(dest); 32 | }); 33 | }; 34 | if (options.noMinify) 35 | onSVGO({data: file}); 36 | else 37 | onSVGO(optimize(file)); 38 | } 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /esm/copy.js: -------------------------------------------------------------------------------- 1 | import {copyFile} from 'fs'; 2 | import {extname} from 'path'; 3 | 4 | import compressed from './compressed.js'; 5 | import compress from './compress.js'; 6 | import headers from './headers.js'; 7 | 8 | compressed.add('.csv'); 9 | compressed.add('.txt'); 10 | compressed.add('.woff2'); 11 | compressed.add('.yml'); 12 | 13 | /** 14 | * Copy a source file into a destination. 15 | * @param {string} source The source file to copy. 16 | * @param {string} dest The destination file. 17 | * @param {Options} [options] Options to deal with extra computation. 18 | * @return {Promise} A promise that resolves with the destination file. 19 | */ 20 | export default (source, dest, /* istanbul ignore next */ options = {}) => 21 | new Promise((res, rej) => { 22 | const onCopy = err => { 23 | if (err) 24 | rej(err); 25 | else if (options.createFiles) { 26 | const ext = extname(source); 27 | if (compressed.has(ext)) 28 | compress(source, dest, ext === '.woff2' ? 'font' : 'text', options) 29 | .then(() => res(dest), rej); 30 | else 31 | headers(source, dest, options.headers) 32 | .then(() => res(dest), rej); 33 | } 34 | else 35 | res(dest); 36 | }; 37 | if (source === dest) 38 | onCopy(null); 39 | else 40 | copyFile(source, dest, onCopy); 41 | }); 42 | -------------------------------------------------------------------------------- /esm/json.js: -------------------------------------------------------------------------------- 1 | import {readFile, writeFile} from 'fs'; 2 | 3 | import compressed from './compressed.js'; 4 | import compress from './compress.js'; 5 | 6 | compressed.add('.json'); 7 | 8 | const {parse, stringify} = JSON; 9 | 10 | /** 11 | * Copy a source file into a destination. 12 | * @param {string} source The source file to copy. 13 | * @param {string} dest The destination file. 14 | * @param {Options} [options] Options to deal with extra computation. 15 | * @return {Promise} A promise that resolves with the destination file. 16 | */ 17 | export default (source, dest, /* istanbul ignore next */ options = {}) => 18 | new Promise((res, rej) => { 19 | readFile(source, (err, data) => { 20 | /* istanbul ignore if */ 21 | if (err) 22 | rej(err); 23 | else { 24 | /* istanbul ignore next */ 25 | try { 26 | const content = options.noMinify || !/[\r\n]\s/.test(data) ? 27 | data : stringify(parse(data.toString())); 28 | writeFile(dest, content, err => { 29 | if (err) 30 | rej(err); 31 | else if (options.createFiles) { 32 | compress(source, dest, 'text', options) 33 | .then(() => res(dest), rej); 34 | } 35 | }); 36 | } 37 | catch (err) { 38 | rej(err); 39 | } 40 | } 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /cjs/svg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {readFile, writeFile} = require('fs'); 3 | 4 | const {optimize} = require('svgo'); 5 | 6 | const compressed = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compressed.js')); 7 | const compress = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compress.js')); 8 | 9 | compressed.add('.svg'); 10 | 11 | /** 12 | * Create a file after minifying it via `svgo`. 13 | * @param {string} source The source SVG file to minify. 14 | * @param {string} dest The minified destination file. 15 | * @param {Options} [options] Options to deal with extra computation. 16 | * @return {Promise} A promise that resolves with the destination file. 17 | */ 18 | module.exports = (source, dest, options = {}) => 19 | new Promise((res, rej) => { 20 | readFile(source, (err, file) => { 21 | if (err) 22 | rej(err); 23 | else { 24 | const onSVGO = ({data}) => { 25 | writeFile(dest, data, err => { 26 | if (err) 27 | rej(err); 28 | else if (options.createFiles) 29 | compress(source, dest, 'text', options) 30 | .then(() => res(dest), rej); 31 | else 32 | res(dest); 33 | }); 34 | }; 35 | if (options.noMinify) 36 | onSVGO({data: file}); 37 | else 38 | onSVGO(optimize(file)); 39 | } 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /esm/html.js: -------------------------------------------------------------------------------- 1 | import {readFile, writeFile} from 'fs'; 2 | 3 | import html from 'html-minifier'; 4 | 5 | import compressed from './compressed.js'; 6 | import compress from './compress.js'; 7 | import htmlArgs from './html-minifier.js'; 8 | 9 | compressed.add('.htm'); 10 | compressed.add('.html'); 11 | 12 | /** 13 | * Create a file after minifying it via `html-minifier`. 14 | * @param {string} source The source HTML file to minify. 15 | * @param {string} dest The minified destination file. 16 | * @param {Options} [options] Options to deal with extra computation. 17 | * @return {Promise} A promise that resolves with the destination file. 18 | */ 19 | export default (source, dest, options = {}) => 20 | new Promise((res, rej) => { 21 | readFile(source, (err, file) => { 22 | if (err) 23 | rej(err); 24 | else { 25 | try { 26 | /* istanbul ignore next */ 27 | const content = options.noMinify ? 28 | file : 29 | html.minify(file.toString(), htmlArgs); 30 | writeFile(dest, content, err => { 31 | if (err) 32 | rej(err); 33 | else if (options.createFiles) 34 | compress(source, dest, 'text', options) 35 | .then(() => res(dest), rej); 36 | else 37 | res(dest); 38 | }); 39 | } 40 | catch (error) { 41 | rej(error); 42 | } 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /cjs/gif.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {execFile} = require('child_process'); 3 | const {copyFile} = require('fs'); 4 | 5 | const umeta = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('umeta')); 6 | 7 | const headers = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./headers.js')); 8 | 9 | const {require: cjs} = umeta(({url: require('url').pathToFileURL(__filename).href})); 10 | 11 | let gifsicle = ''; 12 | try { 13 | gifsicle = cjs('gifsicle'); 14 | } 15 | catch(meh) { 16 | // gifsicle should be installed via npm 17 | } 18 | 19 | /** 20 | * Create a file after optimizing via `gifsicle`. 21 | * @param {string} source The source GIF file to optimize. 22 | * @param {string} dest The optimized destination file. 23 | * @param {Options} [options] Options to deal with extra computation. 24 | * @return {Promise} A promise that resolves with the destination file. 25 | */ 26 | module.exports = (source, dest, /* istanbul ignore next */ options = {}) => 27 | new Promise((res, rej) => { 28 | const callback = err => { 29 | if (err) 30 | rej(err); 31 | else if (options.createFiles) 32 | headers(source, dest, options.headers) 33 | .then(() => res(dest), rej); 34 | else 35 | res(dest); 36 | }; 37 | /* istanbul ignore else */ 38 | if (gifsicle) 39 | execFile(gifsicle, ['-o', dest, source], callback); 40 | else 41 | copyFile(source, dest, callback); 42 | }); 43 | -------------------------------------------------------------------------------- /cjs/css.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {readFile, writeFile} = require('fs'); 3 | 4 | const csso = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('csso')); 5 | 6 | const compressed = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compressed.js')); 7 | const compress = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compress.js')); 8 | 9 | compressed.add('.css'); 10 | 11 | /** 12 | * Create a file after minifying via `csso`. 13 | * @param {string} source The source CSS file to minify. 14 | * @param {string} dest The minified destination file. 15 | * @param {Options} [options] Options to deal with extra computation. 16 | * @return {Promise} A promise that resolves with the destination file. 17 | */ 18 | module.exports = (source, dest, options = {}) => new Promise((res, rej) => { 19 | readFile(source, (err, data) => { 20 | if (err) 21 | rej(err); 22 | else { 23 | /* istanbul ignore next */ 24 | const {css} = options.noMinify ? 25 | {css: data} : 26 | csso.minify(data); 27 | // csso apparently has no way to detect errors 28 | writeFile(dest, css, err => { 29 | if (err) 30 | rej(err); 31 | else if (options.createFiles) 32 | compress(source, dest, 'text', options) 33 | .then(() => res(dest), rej); 34 | else 35 | res(dest); 36 | }); 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /cjs/json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {readFile, writeFile} = require('fs'); 3 | 4 | const compressed = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compressed.js')); 5 | const compress = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compress.js')); 6 | 7 | compressed.add('.json'); 8 | 9 | const {parse, stringify} = JSON; 10 | 11 | /** 12 | * Copy a source file into a destination. 13 | * @param {string} source The source file to copy. 14 | * @param {string} dest The destination file. 15 | * @param {Options} [options] Options to deal with extra computation. 16 | * @return {Promise} A promise that resolves with the destination file. 17 | */ 18 | module.exports = (source, dest, /* istanbul ignore next */ options = {}) => 19 | new Promise((res, rej) => { 20 | readFile(source, (err, data) => { 21 | /* istanbul ignore if */ 22 | if (err) 23 | rej(err); 24 | else { 25 | /* istanbul ignore next */ 26 | try { 27 | const content = options.noMinify || !/[\r\n]\s/.test(data) ? 28 | data : stringify(parse(data.toString())); 29 | writeFile(dest, content, err => { 30 | if (err) 31 | rej(err); 32 | else if (options.createFiles) { 33 | compress(source, dest, 'text', options) 34 | .then(() => res(dest), rej); 35 | } 36 | }); 37 | } 38 | catch (err) { 39 | rej(err); 40 | } 41 | } 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /esm/xml.js: -------------------------------------------------------------------------------- 1 | import {readFile, writeFile} from 'fs'; 2 | 3 | import html from 'html-minifier'; 4 | 5 | import compressed from './compressed.js'; 6 | import compress from './compress.js'; 7 | 8 | const xmlArgs = { 9 | caseSensitive: true, 10 | collapseWhitespace: true, 11 | keepClosingSlash: true, 12 | removeComments: true 13 | }; 14 | 15 | compressed.add('.xml'); 16 | 17 | /** 18 | * Create a file after minifying it via `html-minifier`. 19 | * @param {string} source The source XML file to minify. 20 | * @param {string} dest The minified destination file. 21 | * @param {Options} [options] Options to deal with extra computation. 22 | * @return {Promise} A promise that resolves with the destination file. 23 | */ 24 | export default (source, dest, /* istanbul ignore next */options = {}) => 25 | new Promise((res, rej) => { 26 | readFile(source, (err, file) => { 27 | if (err) 28 | rej(err); 29 | else { 30 | try { 31 | const content = options.noMinify ? 32 | file : html.minify(file.toString(), xmlArgs); 33 | writeFile(dest, content, err => { 34 | /* istanbul ignore next */ 35 | if (err) 36 | rej(err); 37 | else if (options.createFiles) 38 | compress(source, dest, 'text', options) 39 | .then(() => res(dest), rej); 40 | else 41 | res(dest); 42 | }); 43 | } 44 | catch (error) { 45 | /* istanbul ignore next */ 46 | rej(error); 47 | } 48 | } 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /cjs/copy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {copyFile} = require('fs'); 3 | const {extname} = require('path'); 4 | 5 | const compressed = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compressed.js')); 6 | const compress = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compress.js')); 7 | const headers = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./headers.js')); 8 | 9 | compressed.add('.csv'); 10 | compressed.add('.txt'); 11 | compressed.add('.woff2'); 12 | compressed.add('.yml'); 13 | 14 | /** 15 | * Copy a source file into a destination. 16 | * @param {string} source The source file to copy. 17 | * @param {string} dest The destination file. 18 | * @param {Options} [options] Options to deal with extra computation. 19 | * @return {Promise} A promise that resolves with the destination file. 20 | */ 21 | module.exports = (source, dest, /* istanbul ignore next */ options = {}) => 22 | new Promise((res, rej) => { 23 | const onCopy = err => { 24 | if (err) 25 | rej(err); 26 | else if (options.createFiles) { 27 | const ext = extname(source); 28 | if (compressed.has(ext)) 29 | compress(source, dest, ext === '.woff2' ? 'font' : 'text', options) 30 | .then(() => res(dest), rej); 31 | else 32 | headers(source, dest, options.headers) 33 | .then(() => res(dest), rej); 34 | } 35 | else 36 | res(dest); 37 | }; 38 | if (source === dest) 39 | onCopy(null); 40 | else 41 | copyFile(source, dest, onCopy); 42 | }); 43 | -------------------------------------------------------------------------------- /cjs/html.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {readFile, writeFile} = require('fs'); 3 | 4 | const html = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('html-minifier')); 5 | 6 | const compressed = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compressed.js')); 7 | const compress = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compress.js')); 8 | const htmlArgs = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./html-minifier.js')); 9 | 10 | compressed.add('.htm'); 11 | compressed.add('.html'); 12 | 13 | /** 14 | * Create a file after minifying it via `html-minifier`. 15 | * @param {string} source The source HTML file to minify. 16 | * @param {string} dest The minified destination file. 17 | * @param {Options} [options] Options to deal with extra computation. 18 | * @return {Promise} A promise that resolves with the destination file. 19 | */ 20 | module.exports = (source, dest, options = {}) => 21 | new Promise((res, rej) => { 22 | readFile(source, (err, file) => { 23 | if (err) 24 | rej(err); 25 | else { 26 | try { 27 | /* istanbul ignore next */ 28 | const content = options.noMinify ? 29 | file : 30 | html.minify(file.toString(), htmlArgs); 31 | writeFile(dest, content, err => { 32 | if (err) 33 | rej(err); 34 | else if (options.createFiles) 35 | compress(source, dest, 'text', options) 36 | .then(() => res(dest), rej); 37 | else 38 | res(dest); 39 | }); 40 | } 41 | catch (error) { 42 | rej(error); 43 | } 44 | } 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /cjs/xml.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {readFile, writeFile} = require('fs'); 3 | 4 | const html = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('html-minifier')); 5 | 6 | const compressed = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compressed.js')); 7 | const compress = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compress.js')); 8 | 9 | const xmlArgs = { 10 | caseSensitive: true, 11 | collapseWhitespace: true, 12 | keepClosingSlash: true, 13 | removeComments: true 14 | }; 15 | 16 | compressed.add('.xml'); 17 | 18 | /** 19 | * Create a file after minifying it via `html-minifier`. 20 | * @param {string} source The source XML file to minify. 21 | * @param {string} dest The minified destination file. 22 | * @param {Options} [options] Options to deal with extra computation. 23 | * @return {Promise} A promise that resolves with the destination file. 24 | */ 25 | module.exports = (source, dest, /* istanbul ignore next */options = {}) => 26 | new Promise((res, rej) => { 27 | readFile(source, (err, file) => { 28 | if (err) 29 | rej(err); 30 | else { 31 | try { 32 | const content = options.noMinify ? 33 | file : html.minify(file.toString(), xmlArgs); 34 | writeFile(dest, content, err => { 35 | /* istanbul ignore next */ 36 | if (err) 37 | rej(err); 38 | else if (options.createFiles) 39 | compress(source, dest, 'text', options) 40 | .then(() => res(dest), rej); 41 | else 42 | res(dest); 43 | }); 44 | } 45 | catch (error) { 46 | /* istanbul ignore next */ 47 | rej(error); 48 | } 49 | } 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ucompress", 3 | "version": "0.22.2", 4 | "description": "A micro, all-in-one, compressor for common Web files", 5 | "main": "./cjs/index.js", 6 | "bin": "./binary.cjs", 7 | "scripts": { 8 | "build": "npm run cjs && npm run test", 9 | "cjs": "ascjs --no-default esm cjs", 10 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 11 | "clean:dest": "rm -rf test/shenanigans && rm -rf test/dest && mkdir -p test/dest", 12 | "clean:prepared": "rm -rf test/prepared/text.txt.* && rm -rf test/prepared/recursive/text.txt.*", 13 | "test": "npm run clean:prepared && npm run clean:dest && nyc node test/index.js" 14 | }, 15 | "keywords": [ 16 | "minify", 17 | "compress", 18 | "html", 19 | "css", 20 | "svg", 21 | "png", 22 | "jpg", 23 | "gif" 24 | ], 25 | "author": "Andrea Giammarchi", 26 | "license": "ISC", 27 | "devDependencies": { 28 | "ascjs": "^5.0.1", 29 | "compression": "^1.7.4", 30 | "coveralls": "^3.1.1", 31 | "essential-md": "^0.3.1", 32 | "express": "^4.17.1", 33 | "nyc": "^15.1.0", 34 | "uhtml": "^2.7.6" 35 | }, 36 | "module": "./esm/index.js", 37 | "type": "module", 38 | "exports": { 39 | "import": "./esm/index.js", 40 | "default": "./cjs/index.js" 41 | }, 42 | "dependencies": { 43 | "csso": "^4.2.0", 44 | "html-minifier": "^4.0.0", 45 | "marked": "^3.0.2", 46 | "mime-types": "^2.1.32", 47 | "minify-html-literals": "^1.3.5", 48 | "pngquant-bin": "^6.0.0", 49 | "sharp": "^0.29.0", 50 | "svgo": "^2.5.0", 51 | "terser": "^5.7.2", 52 | "umap": "^1.0.2", 53 | "umeta": "^0.2.4" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "git+https://github.com/WebReflection/ucompress.git" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/WebReflection/ucompress/issues" 61 | }, 62 | "homepage": "https://github.com/WebReflection/ucompress#readme", 63 | "optionalDependencies": { 64 | "gifsicle": "^5.2.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /esm/png.js: -------------------------------------------------------------------------------- 1 | import {execFile} from 'child_process'; 2 | import {copyFile, unlink} from 'fs'; 3 | 4 | import pngquant from 'pngquant-bin'; 5 | import sharp from 'sharp'; 6 | 7 | import headers from './headers.js'; 8 | 9 | const fit = sharp.fit.inside; 10 | const pngquantArgs = ['--skip-if-larger', '--speed', '1', '-f', '-o']; 11 | const withoutEnlargement = true; 12 | 13 | const optimize = (source, dest) => new Promise((res, rej) => { 14 | execFile(pngquant, pngquantArgs.concat(dest, source), err => { 15 | if (err) { 16 | copyFile(source, dest, err => { 17 | if (err) rej(err); 18 | else res(dest); 19 | }); 20 | } 21 | else 22 | res(dest); 23 | }); 24 | }); 25 | 26 | /** 27 | * Create a file after optimizing via `pngquant`. 28 | * @param {string} source The source PNG file to optimize. 29 | * @param {string} dest The optimized destination file. 30 | * @param {Options} [options] Options to deal with extra computation. 31 | * @return {Promise} A promise that resolves with the destination file. 32 | */ 33 | export default (source, dest, options = {}) => 34 | new Promise((res, rej) => { 35 | const {maxWidth: width, maxHeight: height, createFiles} = options; 36 | const done = () => res(dest); 37 | const walkThrough = () => { 38 | if (createFiles) writeHeaders(dest).then(done, rej); 39 | else done(); 40 | }; 41 | const writeHeaders = dest => headers(source, dest, options.headers); 42 | if (width || height) { 43 | sharp(source) 44 | .resize({width, height, fit, withoutEnlargement}) 45 | .toFile(`${dest}.resized.png`) 46 | .then( 47 | () => optimize(`${dest}.resized.png`, dest).then( 48 | () => { 49 | unlink(`${dest}.resized.png`, err => { 50 | /* istanbul ignore if */ 51 | if (err) rej(err); 52 | else walkThrough(); 53 | }); 54 | }, 55 | rej 56 | ), 57 | rej 58 | ) 59 | ; 60 | } 61 | else 62 | optimize(source, dest).then(walkThrough, rej); 63 | }); 64 | -------------------------------------------------------------------------------- /esm/headers.js: -------------------------------------------------------------------------------- 1 | import {createHash} from 'crypto'; 2 | import {createReadStream, stat, writeFile} from 'fs'; 3 | import {extname} from 'path'; 4 | 5 | import mime from 'mime-types'; 6 | import umap from 'umap'; 7 | 8 | const {lookup} = mime; 9 | const {stringify} = JSON; 10 | 11 | const cache = new Map; 12 | const wrap = umap(cache); 13 | 14 | const getLastModified = source => ( 15 | wrap.get(source) || 16 | wrap.set(source, new Promise((res, rej) => { 17 | stat(source, (err, stats) => { 18 | /* istanbul ignore next */ 19 | if (err) rej(err); 20 | else res(new Date(stats.mtimeMs).toUTCString()); 21 | }); 22 | })) 23 | ); 24 | 25 | const getHash = source => new Promise(res => { 26 | const hash = createHash('sha1'); 27 | const input = createReadStream(source); 28 | input.on('readable', () => { 29 | const data = input.read(); 30 | if (data) 31 | hash.update(data, 'utf-8'); 32 | else 33 | res(hash.digest('base64')); 34 | }); 35 | }); 36 | 37 | export default (source, dest, headers = {}) => new Promise((res, rej) => { 38 | getLastModified(source).then( 39 | lastModified => { 40 | stat(dest, (err, stats) => { 41 | /* istanbul ignore next */ 42 | if (err) rej(err); 43 | else { 44 | const {size} = stats; 45 | const ext = extname(dest.replace(/(?:\.(br|deflate|gzip))?$/, '')); 46 | getHash(dest).then(hash => { 47 | writeFile( 48 | dest + '.json', 49 | stringify({ 50 | 'Accept-Ranges': 'bytes', 51 | 'Cache-Control': 'public, max-age=0', 52 | 'Content-Type': lookup(ext) + ( 53 | /^\.(?:css|html?|js|mjs|json|map|md|txt|xml|yml)$/.test(ext) ? 54 | '; charset=UTF-8' : '' 55 | ), 56 | 'Content-Length': size, 57 | 'Last-Modified': lastModified, 58 | ETag: `"${size.toString(16)}-${hash.substring(0, 16)}"`, 59 | ...headers 60 | }), 61 | err => { 62 | cache.delete(source); 63 | /* istanbul ignore next */ 64 | if (err) rej(err); 65 | else res(dest); 66 | } 67 | ); 68 | }); 69 | } 70 | }); 71 | }, 72 | rej 73 | ); 74 | }); 75 | -------------------------------------------------------------------------------- /cjs/png.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {execFile} = require('child_process'); 3 | const {copyFile, unlink} = require('fs'); 4 | 5 | const pngquant = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('pngquant-bin')); 6 | const sharp = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('sharp')); 7 | 8 | const headers = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./headers.js')); 9 | 10 | const fit = sharp.fit.inside; 11 | const pngquantArgs = ['--skip-if-larger', '--speed', '1', '-f', '-o']; 12 | const withoutEnlargement = true; 13 | 14 | const optimize = (source, dest) => new Promise((res, rej) => { 15 | execFile(pngquant, pngquantArgs.concat(dest, source), err => { 16 | if (err) { 17 | copyFile(source, dest, err => { 18 | if (err) rej(err); 19 | else res(dest); 20 | }); 21 | } 22 | else 23 | res(dest); 24 | }); 25 | }); 26 | 27 | /** 28 | * Create a file after optimizing via `pngquant`. 29 | * @param {string} source The source PNG file to optimize. 30 | * @param {string} dest The optimized destination file. 31 | * @param {Options} [options] Options to deal with extra computation. 32 | * @return {Promise} A promise that resolves with the destination file. 33 | */ 34 | module.exports = (source, dest, options = {}) => 35 | new Promise((res, rej) => { 36 | const {maxWidth: width, maxHeight: height, createFiles} = options; 37 | const done = () => res(dest); 38 | const walkThrough = () => { 39 | if (createFiles) writeHeaders(dest).then(done, rej); 40 | else done(); 41 | }; 42 | const writeHeaders = dest => headers(source, dest, options.headers); 43 | if (width || height) { 44 | sharp(source) 45 | .resize({width, height, fit, withoutEnlargement}) 46 | .toFile(`${dest}.resized.png`) 47 | .then( 48 | () => optimize(`${dest}.resized.png`, dest).then( 49 | () => { 50 | unlink(`${dest}.resized.png`, err => { 51 | /* istanbul ignore if */ 52 | if (err) rej(err); 53 | else walkThrough(); 54 | }); 55 | }, 56 | rej 57 | ), 58 | rej 59 | ) 60 | ; 61 | } 62 | else 63 | optimize(source, dest).then(walkThrough, rej); 64 | }); 65 | -------------------------------------------------------------------------------- /esm/jpg.js: -------------------------------------------------------------------------------- 1 | import {unlink, write, copyFile} from 'fs'; 2 | 3 | import sharp from 'sharp'; 4 | 5 | import headers from './headers.js'; 6 | import blur from './preview.js'; 7 | 8 | const fit = sharp.fit.inside; 9 | const withoutEnlargement = true; 10 | 11 | const optimize = (args, source, dest) => new Promise((res, rej) => { 12 | sharp(source).jpeg(args).toFile(dest).then( 13 | () => res(dest), 14 | () => { 15 | copyFile(source, dest, err => { 16 | /* istanbul ignore else */ 17 | if (err) rej(err); 18 | else res(dest); 19 | }); 20 | } 21 | ); 22 | }); 23 | 24 | /** 25 | * Create a file after optimizing via `sharp`. 26 | * @param {string} source The source JPG/JPEG file to optimize. 27 | * @param {string} dest The optimized destination file. 28 | * @param {Options} [options] Options to deal with extra computation. 29 | * @return {Promise} A promise that resolves with the destination file. 30 | */ 31 | export default (source, dest, /* istanbul ignore next */ options = {}) => 32 | new Promise((res, rej) => { 33 | const {maxWidth: width, maxHeight: height, createFiles, preview} = options; 34 | const done = () => res(dest); 35 | const walkThrough = () => { 36 | if (createFiles) 37 | writeHeaders(dest).then( 38 | () => { 39 | if (preview) 40 | blur(dest).then(dest => writeHeaders(dest).then(done, rej), rej); 41 | else 42 | done(); 43 | }, 44 | rej 45 | ); 46 | /* istanbul ignore next */ 47 | else if (preview) blur(dest).then(done, rej); 48 | else done(); 49 | }; 50 | const writeHeaders = dest => headers(source, dest, options.headers); 51 | const args = {progressive: !preview}; 52 | if (width || height) { 53 | sharp(source) 54 | .resize({width, height, fit, withoutEnlargement}) 55 | .toFile(`${dest}.resized.jpg`) 56 | .then( 57 | () => optimize(args, `${dest}.resized.jpg`, dest).then( 58 | () => { 59 | unlink(`${dest}.resized.jpg`, err => { 60 | /* istanbul ignore if */ 61 | if (err) rej(err); 62 | else walkThrough(); 63 | }); 64 | }, 65 | rej 66 | ), 67 | rej 68 | ) 69 | ; 70 | } 71 | else 72 | optimize(args, source, dest).then(walkThrough, rej); 73 | }); 74 | -------------------------------------------------------------------------------- /cjs/headers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {createHash} = require('crypto'); 3 | const {createReadStream, stat, writeFile} = require('fs'); 4 | const {extname} = require('path'); 5 | 6 | const mime = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('mime-types')); 7 | const umap = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('umap')); 8 | 9 | const {lookup} = mime; 10 | const {stringify} = JSON; 11 | 12 | const cache = new Map; 13 | const wrap = umap(cache); 14 | 15 | const getLastModified = source => ( 16 | wrap.get(source) || 17 | wrap.set(source, new Promise((res, rej) => { 18 | stat(source, (err, stats) => { 19 | /* istanbul ignore next */ 20 | if (err) rej(err); 21 | else res(new Date(stats.mtimeMs).toUTCString()); 22 | }); 23 | })) 24 | ); 25 | 26 | const getHash = source => new Promise(res => { 27 | const hash = createHash('sha1'); 28 | const input = createReadStream(source); 29 | input.on('readable', () => { 30 | const data = input.read(); 31 | if (data) 32 | hash.update(data, 'utf-8'); 33 | else 34 | res(hash.digest('base64')); 35 | }); 36 | }); 37 | 38 | module.exports = (source, dest, headers = {}) => new Promise((res, rej) => { 39 | getLastModified(source).then( 40 | lastModified => { 41 | stat(dest, (err, stats) => { 42 | /* istanbul ignore next */ 43 | if (err) rej(err); 44 | else { 45 | const {size} = stats; 46 | const ext = extname(dest.replace(/(?:\.(br|deflate|gzip))?$/, '')); 47 | getHash(dest).then(hash => { 48 | writeFile( 49 | dest + '.json', 50 | stringify({ 51 | 'Accept-Ranges': 'bytes', 52 | 'Cache-Control': 'public, max-age=0', 53 | 'Content-Type': lookup(ext) + ( 54 | /^\.(?:css|html?|js|mjs|json|map|md|txt|xml|yml)$/.test(ext) ? 55 | '; charset=UTF-8' : '' 56 | ), 57 | 'Content-Length': size, 58 | 'Last-Modified': lastModified, 59 | ETag: `"${size.toString(16)}-${hash.substring(0, 16)}"`, 60 | ...headers 61 | }), 62 | err => { 63 | cache.delete(source); 64 | /* istanbul ignore next */ 65 | if (err) rej(err); 66 | else res(dest); 67 | } 68 | ); 69 | }); 70 | } 71 | }); 72 | }, 73 | rej 74 | ); 75 | }); 76 | -------------------------------------------------------------------------------- /cjs/jpg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {unlink, write, copyFile} = require('fs'); 3 | 4 | const sharp = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('sharp')); 5 | 6 | const headers = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./headers.js')); 7 | const blur = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./preview.js')); 8 | 9 | const fit = sharp.fit.inside; 10 | const withoutEnlargement = true; 11 | 12 | const optimize = (args, source, dest) => new Promise((res, rej) => { 13 | sharp(source).jpeg(args).toFile(dest).then( 14 | () => res(dest), 15 | () => { 16 | copyFile(source, dest, err => { 17 | /* istanbul ignore else */ 18 | if (err) rej(err); 19 | else res(dest); 20 | }); 21 | } 22 | ); 23 | }); 24 | 25 | /** 26 | * Create a file after optimizing via `sharp`. 27 | * @param {string} source The source JPG/JPEG file to optimize. 28 | * @param {string} dest The optimized destination file. 29 | * @param {Options} [options] Options to deal with extra computation. 30 | * @return {Promise} A promise that resolves with the destination file. 31 | */ 32 | module.exports = (source, dest, /* istanbul ignore next */ options = {}) => 33 | new Promise((res, rej) => { 34 | const {maxWidth: width, maxHeight: height, createFiles, preview} = options; 35 | const done = () => res(dest); 36 | const walkThrough = () => { 37 | if (createFiles) 38 | writeHeaders(dest).then( 39 | () => { 40 | if (preview) 41 | blur(dest).then(dest => writeHeaders(dest).then(done, rej), rej); 42 | else 43 | done(); 44 | }, 45 | rej 46 | ); 47 | /* istanbul ignore next */ 48 | else if (preview) blur(dest).then(done, rej); 49 | else done(); 50 | }; 51 | const writeHeaders = dest => headers(source, dest, options.headers); 52 | const args = {progressive: !preview}; 53 | if (width || height) { 54 | sharp(source) 55 | .resize({width, height, fit, withoutEnlargement}) 56 | .toFile(`${dest}.resized.jpg`) 57 | .then( 58 | () => optimize(args, `${dest}.resized.jpg`, dest).then( 59 | () => { 60 | unlink(`${dest}.resized.jpg`, err => { 61 | /* istanbul ignore if */ 62 | if (err) rej(err); 63 | else walkThrough(); 64 | }); 65 | }, 66 | rej 67 | ), 68 | rej 69 | ) 70 | ; 71 | } 72 | else 73 | optimize(args, source, dest).then(walkThrough, rej); 74 | }); 75 | -------------------------------------------------------------------------------- /esm/compress.js: -------------------------------------------------------------------------------- 1 | import {createReadStream, createWriteStream, stat} from 'fs'; 2 | import {pipeline} from 'stream'; 3 | import zlib from 'zlib'; 4 | 5 | import headers from './headers.js'; 6 | 7 | const { 8 | BROTLI_MAX_QUALITY, 9 | BROTLI_MODE_GENERIC, 10 | BROTLI_MODE_FONT, 11 | BROTLI_MODE_TEXT, 12 | BROTLI_PARAM_MODE, 13 | BROTLI_PARAM_QUALITY, 14 | BROTLI_PARAM_SIZE_HINT, 15 | Z_BEST_COMPRESSION 16 | } = zlib.constants; 17 | 18 | const { 19 | createBrotliCompress, 20 | createDeflate, 21 | createGzip 22 | } = zlib; 23 | 24 | const zlibDefaultOptions = { 25 | level: Z_BEST_COMPRESSION 26 | }; 27 | 28 | const br = (source, mode) => new Promise((res, rej) => { 29 | const dest = source + '.br'; 30 | stat(source, (err, stats) => { 31 | /* istanbul ignore next */ 32 | if (err) rej(err); 33 | else pipeline( 34 | createReadStream(source), 35 | createBrotliCompress({ 36 | [BROTLI_PARAM_SIZE_HINT]: stats.size, 37 | [BROTLI_PARAM_QUALITY]: BROTLI_MAX_QUALITY, 38 | [BROTLI_PARAM_MODE]: mode == 'text' ? 39 | BROTLI_MODE_TEXT : ( 40 | mode === 'font' ? 41 | BROTLI_MODE_FONT : 42 | /* istanbul ignore next */ 43 | BROTLI_MODE_GENERIC 44 | ) 45 | }), 46 | createWriteStream(dest), 47 | err => { 48 | /* istanbul ignore next */ 49 | if (err) rej(err); 50 | else res(dest); 51 | } 52 | ); 53 | }); 54 | }); 55 | 56 | const deflate = source => new Promise((res, rej) => { 57 | const dest = source + '.deflate'; 58 | pipeline( 59 | createReadStream(source), 60 | createDeflate(zlibDefaultOptions), 61 | createWriteStream(dest), 62 | err => { 63 | /* istanbul ignore next */ 64 | if (err) rej(err); 65 | else res(dest); 66 | } 67 | ); 68 | }); 69 | 70 | const gzip = source => new Promise((res, rej) => { 71 | const dest = source + '.gzip'; 72 | pipeline( 73 | createReadStream(source), 74 | createGzip(zlibDefaultOptions), 75 | createWriteStream(dest), 76 | err => { 77 | /* istanbul ignore next */ 78 | if (err) rej(err); 79 | else res(dest); 80 | } 81 | ); 82 | }); 83 | 84 | export default (source, dest, mode, options) => Promise.all([ 85 | br(dest, mode), 86 | gzip(dest), 87 | deflate(dest) 88 | ]).then(([br, gzip, deflate]) => Promise.all([ 89 | headers(source, dest, options.headers), 90 | headers(source, br, { 91 | ...options.headers, 92 | 'Content-Encoding': 'br' 93 | }), 94 | headers(source, gzip, { 95 | ...options.headers, 96 | 'Content-Encoding': 'gzip' 97 | }), 98 | headers(source, deflate, { 99 | ...options.headers, 100 | 'Content-Encoding': 'deflate' 101 | }) 102 | ])); 103 | -------------------------------------------------------------------------------- /cjs/compress.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {createReadStream, createWriteStream, stat} = require('fs'); 3 | const {pipeline} = require('stream'); 4 | const zlib = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('zlib')); 5 | 6 | const headers = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./headers.js')); 7 | 8 | const { 9 | BROTLI_MAX_QUALITY, 10 | BROTLI_MODE_GENERIC, 11 | BROTLI_MODE_FONT, 12 | BROTLI_MODE_TEXT, 13 | BROTLI_PARAM_MODE, 14 | BROTLI_PARAM_QUALITY, 15 | BROTLI_PARAM_SIZE_HINT, 16 | Z_BEST_COMPRESSION 17 | } = zlib.constants; 18 | 19 | const { 20 | createBrotliCompress, 21 | createDeflate, 22 | createGzip 23 | } = zlib; 24 | 25 | const zlibDefaultOptions = { 26 | level: Z_BEST_COMPRESSION 27 | }; 28 | 29 | const br = (source, mode) => new Promise((res, rej) => { 30 | const dest = source + '.br'; 31 | stat(source, (err, stats) => { 32 | /* istanbul ignore next */ 33 | if (err) rej(err); 34 | else pipeline( 35 | createReadStream(source), 36 | createBrotliCompress({ 37 | [BROTLI_PARAM_SIZE_HINT]: stats.size, 38 | [BROTLI_PARAM_QUALITY]: BROTLI_MAX_QUALITY, 39 | [BROTLI_PARAM_MODE]: mode == 'text' ? 40 | BROTLI_MODE_TEXT : ( 41 | mode === 'font' ? 42 | BROTLI_MODE_FONT : 43 | /* istanbul ignore next */ 44 | BROTLI_MODE_GENERIC 45 | ) 46 | }), 47 | createWriteStream(dest), 48 | err => { 49 | /* istanbul ignore next */ 50 | if (err) rej(err); 51 | else res(dest); 52 | } 53 | ); 54 | }); 55 | }); 56 | 57 | const deflate = source => new Promise((res, rej) => { 58 | const dest = source + '.deflate'; 59 | pipeline( 60 | createReadStream(source), 61 | createDeflate(zlibDefaultOptions), 62 | createWriteStream(dest), 63 | err => { 64 | /* istanbul ignore next */ 65 | if (err) rej(err); 66 | else res(dest); 67 | } 68 | ); 69 | }); 70 | 71 | const gzip = source => new Promise((res, rej) => { 72 | const dest = source + '.gzip'; 73 | pipeline( 74 | createReadStream(source), 75 | createGzip(zlibDefaultOptions), 76 | createWriteStream(dest), 77 | err => { 78 | /* istanbul ignore next */ 79 | if (err) rej(err); 80 | else res(dest); 81 | } 82 | ); 83 | }); 84 | 85 | module.exports = (source, dest, mode, options) => Promise.all([ 86 | br(dest, mode), 87 | gzip(dest), 88 | deflate(dest) 89 | ]).then(([br, gzip, deflate]) => Promise.all([ 90 | headers(source, dest, options.headers), 91 | headers(source, br, { 92 | ...options.headers, 93 | 'Content-Encoding': 'br' 94 | }), 95 | headers(source, gzip, { 96 | ...options.headers, 97 | 'Content-Encoding': 'gzip' 98 | }), 99 | headers(source, deflate, { 100 | ...options.headers, 101 | 'Content-Encoding': 'deflate' 102 | }) 103 | ])); 104 | -------------------------------------------------------------------------------- /esm/md.js: -------------------------------------------------------------------------------- 1 | import {copyFile, readFile, writeFile} from 'fs'; 2 | import {basename} from 'path'; 3 | 4 | import html from 'html-minifier'; 5 | import marked from 'marked'; 6 | 7 | import compressed from './compressed.js'; 8 | import compress from './compress.js'; 9 | import htmlArgs from './html-minifier.js'; 10 | 11 | compressed.add('.md'); 12 | 13 | marked.setOptions({ 14 | renderer: new marked.Renderer(), 15 | pedantic: false, 16 | gfm: true, 17 | breaks: false, 18 | sanitize: false, 19 | smartLists: true, 20 | smartypants: false, 21 | xhtml: false 22 | }); 23 | 24 | /** 25 | * Copy a source file into a destination. 26 | * @param {string} source The source file to copy. 27 | * @param {string} dest The destination file. 28 | * @param {Options} [options] Options to deal with extra computation. 29 | * @return {Promise} A promise that resolves with the destination file. 30 | */ 31 | export default (source, dest, /* istanbul ignore next */ options = {}) => 32 | new Promise((res, rej) => { 33 | const {preview} = options; 34 | const onCopy = err => { 35 | if (err) 36 | rej(err); 37 | else if (options.createFiles) { 38 | compress(source, dest, 'text', options) 39 | .then(() => res(dest), rej); 40 | } 41 | else 42 | res(dest); 43 | }; 44 | const onPreview = () => { 45 | /* istanbul ignore if */ 46 | if (source === dest) 47 | onCopy(null); 48 | else 49 | copyFile(source, dest, onCopy); 50 | }; 51 | if (preview) { 52 | readFile(source, (err, data) => { 53 | /* istanbul ignore if */ 54 | if (err) 55 | rej(err); 56 | else { 57 | marked(data.toString(), (err, md) => { 58 | /* istanbul ignore if */ 59 | if (err) 60 | rej(err); 61 | else { 62 | const htmlPreview = dest.replace(/\.md$/i, '.md.preview.html'); 63 | writeFile( 64 | htmlPreview, 65 | html.minify( 66 | ` 67 | 68 | 69 | 70 | 71 | Preview: ${basename(source)} 72 | 73 | 74 | ${md} 75 | `, 76 | htmlArgs 77 | ), 78 | err => { 79 | /* istanbul ignore if */ 80 | if (err) 81 | rej(err); 82 | /* istanbul ignore else */ 83 | else if (options.createFiles) { 84 | compress(source, htmlPreview, 'text', options) 85 | .then(() => onPreview(), rej); 86 | } 87 | else 88 | onPreview(); 89 | } 90 | ); 91 | } 92 | }); 93 | } 94 | }); 95 | } 96 | else 97 | onPreview(); 98 | }); 99 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | import {stat, readdir} from 'fs'; 2 | 3 | import {extname, join} from 'path'; 4 | 5 | import compressed from './compressed.js'; 6 | import copy from './copy.js'; 7 | import css from './css.js'; 8 | import gif from './gif.js'; 9 | import html from './html.js'; 10 | import jpg from './jpg.js'; 11 | import js from './js.js'; 12 | import json from './json.js'; 13 | import md from './md.js'; 14 | import png from './png.js'; 15 | import svg from './svg.js'; 16 | import xml from './xml.js'; 17 | 18 | const crawl = (source, options) => new Promise((res, rej) => { 19 | stat(source, (err, stat) => { 20 | /* istanbul ignore if */ 21 | if (err) 22 | rej(err); 23 | else { 24 | if (stat.isFile()) 25 | copy(source, source, options).then(res, rej); 26 | /* istanbul ignore else */ 27 | else if (stat.isDirectory()) 28 | readdir(source, (err, files) => { 29 | /* istanbul ignore if */ 30 | if (err) 31 | rej(err); 32 | else 33 | Promise.all(files 34 | .filter(file => !/^[._]/.test(file)) 35 | .map(file => crawl(join(source, file), options)) 36 | ).then(res, rej); 37 | }); 38 | } 39 | }); 40 | }); 41 | 42 | /** 43 | * Create a file after minifying or optimizing it, when possible. 44 | * @param {string} source The source file to optimize. 45 | * @param {string} dest The optimized destination file. 46 | * @param {Options} [options] Options to deal with extra computation. 47 | * @return {Promise} A promise that resolves with the destination file. 48 | */ 49 | const ucompress = (source, dest, options = {}) => { 50 | let method = copy; 51 | switch (extname(source).toLowerCase()) { 52 | case '.css': 53 | method = css; 54 | break; 55 | case '.gif': 56 | method = gif; 57 | break; 58 | case '.html': 59 | /* istanbul ignore next */ 60 | case '.htm': 61 | method = html; 62 | break; 63 | case '.jpg': 64 | case '.jpeg': 65 | method = jpg; 66 | break; 67 | case '.js': 68 | case '.mjs': 69 | method = js; 70 | break; 71 | case '.json': 72 | method = json; 73 | break; 74 | case '.md': 75 | method = md; 76 | break; 77 | case '.png': 78 | method = png; 79 | break; 80 | case '.svg': 81 | method = svg; 82 | break; 83 | case '.xml': 84 | method = xml; 85 | break; 86 | } 87 | return method(source, dest, options); 88 | }; 89 | 90 | ucompress.compressed = new Set([...compressed]); 91 | 92 | ucompress.createHeaders = (source, headers = {}) => 93 | crawl(source, {createFiles: true, headers}); 94 | 95 | ucompress.copy = copy; 96 | ucompress.css = css; 97 | ucompress.gif = gif; 98 | ucompress.html = html; 99 | ucompress.htm = html; 100 | ucompress.jpg = jpg; 101 | ucompress.jpeg = jpg; 102 | ucompress.js = js; 103 | ucompress.mjs = js; 104 | ucompress.json = json; 105 | ucompress.md = md; 106 | ucompress.png = png; 107 | ucompress.svg = svg; 108 | ucompress.xml = xml; 109 | 110 | export default ucompress; 111 | -------------------------------------------------------------------------------- /test/source/text.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum vel mi non eros lobortis gravida. Ut non faucibus sapien, a ornare ex. Aliquam arcu turpis, varius et scelerisque ac, hendrerit quis ligula. Nulla sollicitudin egestas mi, vitae rhoncus diam vestibulum vel. Vestibulum posuere ligula dui. Fusce vehicula risus in maximus sollicitudin. In facilisis dui ac sem porttitor faucibus. Mauris vel lorem sed ante mattis bibendum. Curabitur sed ex sed quam vestibulum dictum. 2 | 3 | Sed est tortor, tempor id diam non, malesuada tincidunt tellus. In in condimentum nisi, eu ultricies nunc. Donec nec ornare sapien, in feugiat elit. Donec sed semper justo. Etiam sed aliquet nisi, et condimentum ligula. Fusce interdum, quam ac molestie porttitor, purus nisi auctor nunc, id dapibus orci urna sed augue. Sed non pretium ligula. Phasellus velit mauris, rhoncus eget tellus et, iaculis euismod metus. Nunc pharetra lacinia purus. Praesent a purus aliquam, facilisis nisl vitae, efficitur turpis. Aenean tincidunt sodales elit vitae facilisis. Fusce ac placerat ante. Pellentesque quis massa tempor, posuere elit ut, dignissim lectus. Quisque blandit, tellus id placerat rhoncus, nunc eros facilisis neque, ut efficitur tellus dolor vel tortor. Donec a posuere sapien, ut consectetur ipsum. 4 | 5 | Sed varius orci sed mauris dictum, non ullamcorper urna eleifend. Phasellus ac cursus enim. Nullam est erat, fringilla aliquet pellentesque vel, efficitur nec lorem. Donec in dui et turpis congue gravida. Curabitur suscipit, enim eget imperdiet scelerisque, mauris nibh fringilla mi, ut rutrum justo enim ut elit. Aenean sed erat suscipit, vehicula ex a, gravida purus. Praesent nec ipsum nisl. Quisque interdum purus id est iaculis eleifend. Etiam sagittis aliquam erat, ut sagittis dolor porttitor sed. Mauris purus sapien, tincidunt eu neque eget, tristique pellentesque risus. Sed dictum accumsan tellus id volutpat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec tempor, metus id bibendum ullamcorper, metus libero faucibus ante, sed hendrerit risus eros non mi. 6 | 7 | Aliquam hendrerit ultrices eros vitae porttitor. Ut porttitor, felis at molestie venenatis, odio quam accumsan ligula, id congue tellus tortor pellentesque tellus. Aliquam erat volutpat. Curabitur ut lectus volutpat, cursus sem eget, sollicitudin neque. Aliquam condimentum, nisi dapibus rutrum rhoncus, mi nibh pharetra est, a aliquam metus lorem in ante. Etiam quis dui tincidunt, tincidunt lectus vitae, gravida quam. Etiam non tempus metus, ac ultricies nibh. Cras suscipit posuere urna at consectetur. Pellentesque hendrerit nec eros quis tempor. Mauris egestas turpis risus, ut commodo nisi vulputate sit amet. Praesent fermentum ligula ac risus placerat congue at ac est. Praesent tincidunt sem et nisi suscipit, nec consequat sem laoreet. Curabitur at egestas dui, eu placerat erat. 8 | 9 | Sed suscipit suscipit mi, in laoreet nisl eleifend quis. Interdum et malesuada fames ac ante ipsum primis in faucibus. In hac habitasse platea dictumst. Sed tincidunt aliquet enim, vitae ultrices mauris faucibus vitae. Nulla ornare nisi sit amet ultricies lacinia. Sed volutpat malesuada magna at pulvinar. Nullam congue vel enim rhoncus hendrerit. Maecenas porta est at eleifend viverra. Vestibulum at orci tempus tellus ornare convallis vel non diam. -------------------------------------------------------------------------------- /test/prepared/text.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum vel mi non eros lobortis gravida. Ut non faucibus sapien, a ornare ex. Aliquam arcu turpis, varius et scelerisque ac, hendrerit quis ligula. Nulla sollicitudin egestas mi, vitae rhoncus diam vestibulum vel. Vestibulum posuere ligula dui. Fusce vehicula risus in maximus sollicitudin. In facilisis dui ac sem porttitor faucibus. Mauris vel lorem sed ante mattis bibendum. Curabitur sed ex sed quam vestibulum dictum. 2 | 3 | Sed est tortor, tempor id diam non, malesuada tincidunt tellus. In in condimentum nisi, eu ultricies nunc. Donec nec ornare sapien, in feugiat elit. Donec sed semper justo. Etiam sed aliquet nisi, et condimentum ligula. Fusce interdum, quam ac molestie porttitor, purus nisi auctor nunc, id dapibus orci urna sed augue. Sed non pretium ligula. Phasellus velit mauris, rhoncus eget tellus et, iaculis euismod metus. Nunc pharetra lacinia purus. Praesent a purus aliquam, facilisis nisl vitae, efficitur turpis. Aenean tincidunt sodales elit vitae facilisis. Fusce ac placerat ante. Pellentesque quis massa tempor, posuere elit ut, dignissim lectus. Quisque blandit, tellus id placerat rhoncus, nunc eros facilisis neque, ut efficitur tellus dolor vel tortor. Donec a posuere sapien, ut consectetur ipsum. 4 | 5 | Sed varius orci sed mauris dictum, non ullamcorper urna eleifend. Phasellus ac cursus enim. Nullam est erat, fringilla aliquet pellentesque vel, efficitur nec lorem. Donec in dui et turpis congue gravida. Curabitur suscipit, enim eget imperdiet scelerisque, mauris nibh fringilla mi, ut rutrum justo enim ut elit. Aenean sed erat suscipit, vehicula ex a, gravida purus. Praesent nec ipsum nisl. Quisque interdum purus id est iaculis eleifend. Etiam sagittis aliquam erat, ut sagittis dolor porttitor sed. Mauris purus sapien, tincidunt eu neque eget, tristique pellentesque risus. Sed dictum accumsan tellus id volutpat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec tempor, metus id bibendum ullamcorper, metus libero faucibus ante, sed hendrerit risus eros non mi. 6 | 7 | Aliquam hendrerit ultrices eros vitae porttitor. Ut porttitor, felis at molestie venenatis, odio quam accumsan ligula, id congue tellus tortor pellentesque tellus. Aliquam erat volutpat. Curabitur ut lectus volutpat, cursus sem eget, sollicitudin neque. Aliquam condimentum, nisi dapibus rutrum rhoncus, mi nibh pharetra est, a aliquam metus lorem in ante. Etiam quis dui tincidunt, tincidunt lectus vitae, gravida quam. Etiam non tempus metus, ac ultricies nibh. Cras suscipit posuere urna at consectetur. Pellentesque hendrerit nec eros quis tempor. Mauris egestas turpis risus, ut commodo nisi vulputate sit amet. Praesent fermentum ligula ac risus placerat congue at ac est. Praesent tincidunt sem et nisi suscipit, nec consequat sem laoreet. Curabitur at egestas dui, eu placerat erat. 8 | 9 | Sed suscipit suscipit mi, in laoreet nisl eleifend quis. Interdum et malesuada fames ac ante ipsum primis in faucibus. In hac habitasse platea dictumst. Sed tincidunt aliquet enim, vitae ultrices mauris faucibus vitae. Nulla ornare nisi sit amet ultricies lacinia. Sed volutpat malesuada magna at pulvinar. Nullam congue vel enim rhoncus hendrerit. Maecenas porta est at eleifend viverra. Vestibulum at orci tempus tellus ornare convallis vel non diam. -------------------------------------------------------------------------------- /test/prepared/recursive/text.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum vel mi non eros lobortis gravida. Ut non faucibus sapien, a ornare ex. Aliquam arcu turpis, varius et scelerisque ac, hendrerit quis ligula. Nulla sollicitudin egestas mi, vitae rhoncus diam vestibulum vel. Vestibulum posuere ligula dui. Fusce vehicula risus in maximus sollicitudin. In facilisis dui ac sem porttitor faucibus. Mauris vel lorem sed ante mattis bibendum. Curabitur sed ex sed quam vestibulum dictum. 2 | 3 | Sed est tortor, tempor id diam non, malesuada tincidunt tellus. In in condimentum nisi, eu ultricies nunc. Donec nec ornare sapien, in feugiat elit. Donec sed semper justo. Etiam sed aliquet nisi, et condimentum ligula. Fusce interdum, quam ac molestie porttitor, purus nisi auctor nunc, id dapibus orci urna sed augue. Sed non pretium ligula. Phasellus velit mauris, rhoncus eget tellus et, iaculis euismod metus. Nunc pharetra lacinia purus. Praesent a purus aliquam, facilisis nisl vitae, efficitur turpis. Aenean tincidunt sodales elit vitae facilisis. Fusce ac placerat ante. Pellentesque quis massa tempor, posuere elit ut, dignissim lectus. Quisque blandit, tellus id placerat rhoncus, nunc eros facilisis neque, ut efficitur tellus dolor vel tortor. Donec a posuere sapien, ut consectetur ipsum. 4 | 5 | Sed varius orci sed mauris dictum, non ullamcorper urna eleifend. Phasellus ac cursus enim. Nullam est erat, fringilla aliquet pellentesque vel, efficitur nec lorem. Donec in dui et turpis congue gravida. Curabitur suscipit, enim eget imperdiet scelerisque, mauris nibh fringilla mi, ut rutrum justo enim ut elit. Aenean sed erat suscipit, vehicula ex a, gravida purus. Praesent nec ipsum nisl. Quisque interdum purus id est iaculis eleifend. Etiam sagittis aliquam erat, ut sagittis dolor porttitor sed. Mauris purus sapien, tincidunt eu neque eget, tristique pellentesque risus. Sed dictum accumsan tellus id volutpat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec tempor, metus id bibendum ullamcorper, metus libero faucibus ante, sed hendrerit risus eros non mi. 6 | 7 | Aliquam hendrerit ultrices eros vitae porttitor. Ut porttitor, felis at molestie venenatis, odio quam accumsan ligula, id congue tellus tortor pellentesque tellus. Aliquam erat volutpat. Curabitur ut lectus volutpat, cursus sem eget, sollicitudin neque. Aliquam condimentum, nisi dapibus rutrum rhoncus, mi nibh pharetra est, a aliquam metus lorem in ante. Etiam quis dui tincidunt, tincidunt lectus vitae, gravida quam. Etiam non tempus metus, ac ultricies nibh. Cras suscipit posuere urna at consectetur. Pellentesque hendrerit nec eros quis tempor. Mauris egestas turpis risus, ut commodo nisi vulputate sit amet. Praesent fermentum ligula ac risus placerat congue at ac est. Praesent tincidunt sem et nisi suscipit, nec consequat sem laoreet. Curabitur at egestas dui, eu placerat erat. 8 | 9 | Sed suscipit suscipit mi, in laoreet nisl eleifend quis. Interdum et malesuada fames ac ante ipsum primis in faucibus. In hac habitasse platea dictumst. Sed tincidunt aliquet enim, vitae ultrices mauris faucibus vitae. Nulla ornare nisi sit amet ultricies lacinia. Sed volutpat malesuada magna at pulvinar. Nullam congue vel enim rhoncus hendrerit. Maecenas porta est at eleifend viverra. Vestibulum at orci tempus tellus ornare convallis vel non diam. -------------------------------------------------------------------------------- /cjs/md.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {copyFile, readFile, writeFile} = require('fs'); 3 | const {basename} = require('path'); 4 | 5 | const html = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('html-minifier')); 6 | const marked = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('marked')); 7 | 8 | const compressed = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compressed.js')); 9 | const compress = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compress.js')); 10 | const htmlArgs = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./html-minifier.js')); 11 | 12 | compressed.add('.md'); 13 | 14 | marked.setOptions({ 15 | renderer: new marked.Renderer(), 16 | pedantic: false, 17 | gfm: true, 18 | breaks: false, 19 | sanitize: false, 20 | smartLists: true, 21 | smartypants: false, 22 | xhtml: false 23 | }); 24 | 25 | /** 26 | * Copy a source file into a destination. 27 | * @param {string} source The source file to copy. 28 | * @param {string} dest The destination file. 29 | * @param {Options} [options] Options to deal with extra computation. 30 | * @return {Promise} A promise that resolves with the destination file. 31 | */ 32 | module.exports = (source, dest, /* istanbul ignore next */ options = {}) => 33 | new Promise((res, rej) => { 34 | const {preview} = options; 35 | const onCopy = err => { 36 | if (err) 37 | rej(err); 38 | else if (options.createFiles) { 39 | compress(source, dest, 'text', options) 40 | .then(() => res(dest), rej); 41 | } 42 | else 43 | res(dest); 44 | }; 45 | const onPreview = () => { 46 | /* istanbul ignore if */ 47 | if (source === dest) 48 | onCopy(null); 49 | else 50 | copyFile(source, dest, onCopy); 51 | }; 52 | if (preview) { 53 | readFile(source, (err, data) => { 54 | /* istanbul ignore if */ 55 | if (err) 56 | rej(err); 57 | else { 58 | marked(data.toString(), (err, md) => { 59 | /* istanbul ignore if */ 60 | if (err) 61 | rej(err); 62 | else { 63 | const htmlPreview = dest.replace(/\.md$/i, '.md.preview.html'); 64 | writeFile( 65 | htmlPreview, 66 | html.minify( 67 | ` 68 | 69 | 70 | 71 | 72 | Preview: ${basename(source)} 73 | 74 | 75 | ${md} 76 | `, 77 | htmlArgs 78 | ), 79 | err => { 80 | /* istanbul ignore if */ 81 | if (err) 82 | rej(err); 83 | /* istanbul ignore else */ 84 | else if (options.createFiles) { 85 | compress(source, htmlPreview, 'text', options) 86 | .then(() => onPreview(), rej); 87 | } 88 | else 89 | onPreview(); 90 | } 91 | ); 92 | } 93 | }); 94 | } 95 | }); 96 | } 97 | else 98 | onPreview(); 99 | }); 100 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {stat, readdir} = require('fs'); 3 | 4 | const {extname, join} = require('path'); 5 | 6 | const compressed = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compressed.js')); 7 | const copy = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./copy.js')); 8 | const css = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./css.js')); 9 | const gif = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./gif.js')); 10 | const html = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./html.js')); 11 | const jpg = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./jpg.js')); 12 | const js = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./js.js')); 13 | const json = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./json.js')); 14 | const md = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./md.js')); 15 | const png = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./png.js')); 16 | const svg = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./svg.js')); 17 | const xml = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./xml.js')); 18 | 19 | const crawl = (source, options) => new Promise((res, rej) => { 20 | stat(source, (err, stat) => { 21 | /* istanbul ignore if */ 22 | if (err) 23 | rej(err); 24 | else { 25 | if (stat.isFile()) 26 | copy(source, source, options).then(res, rej); 27 | /* istanbul ignore else */ 28 | else if (stat.isDirectory()) 29 | readdir(source, (err, files) => { 30 | /* istanbul ignore if */ 31 | if (err) 32 | rej(err); 33 | else 34 | Promise.all(files 35 | .filter(file => !/^[._]/.test(file)) 36 | .map(file => crawl(join(source, file), options)) 37 | ).then(res, rej); 38 | }); 39 | } 40 | }); 41 | }); 42 | 43 | /** 44 | * Create a file after minifying or optimizing it, when possible. 45 | * @param {string} source The source file to optimize. 46 | * @param {string} dest The optimized destination file. 47 | * @param {Options} [options] Options to deal with extra computation. 48 | * @return {Promise} A promise that resolves with the destination file. 49 | */ 50 | const ucompress = (source, dest, options = {}) => { 51 | let method = copy; 52 | switch (extname(source).toLowerCase()) { 53 | case '.css': 54 | method = css; 55 | break; 56 | case '.gif': 57 | method = gif; 58 | break; 59 | case '.html': 60 | /* istanbul ignore next */ 61 | case '.htm': 62 | method = html; 63 | break; 64 | case '.jpg': 65 | case '.jpeg': 66 | method = jpg; 67 | break; 68 | case '.js': 69 | case '.mjs': 70 | method = js; 71 | break; 72 | case '.json': 73 | method = json; 74 | break; 75 | case '.md': 76 | method = md; 77 | break; 78 | case '.png': 79 | method = png; 80 | break; 81 | case '.svg': 82 | method = svg; 83 | break; 84 | case '.xml': 85 | method = xml; 86 | break; 87 | } 88 | return method(source, dest, options); 89 | }; 90 | 91 | ucompress.compressed = new Set([...compressed]); 92 | 93 | ucompress.createHeaders = (source, headers = {}) => 94 | crawl(source, {createFiles: true, headers}); 95 | 96 | ucompress.copy = copy; 97 | ucompress.css = css; 98 | ucompress.gif = gif; 99 | ucompress.html = html; 100 | ucompress.htm = html; 101 | ucompress.jpg = jpg; 102 | ucompress.jpeg = jpg; 103 | ucompress.js = js; 104 | ucompress.mjs = js; 105 | ucompress.json = json; 106 | ucompress.md = md; 107 | ucompress.png = png; 108 | ucompress.svg = svg; 109 | ucompress.xml = xml; 110 | 111 | module.exports = ucompress; 112 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const {readdir} = require('fs'); 2 | const {basename, join} = require('path'); 3 | const {log, ok, error} = require('essential-md'); 4 | const ucompress = require('../cjs'); 5 | 6 | const TIMEOUT = 1000; 7 | 8 | log`# ucompress test`; 9 | 10 | 11 | 12 | // regular test for each kind of file 13 | readdir(join(__dirname, 'source'), (_, files) => { 14 | for (const file of files) { 15 | ucompress( 16 | join(__dirname, 'source', file), 17 | join(__dirname, 'dest', file) 18 | ) 19 | .then(dest => ok(`\`${basename(dest)}\``)) 20 | .catch(err => { 21 | error`\`\`\`${err}\`\`\``; 22 | process.exit(1); 23 | }); 24 | } 25 | }); 26 | 27 | setTimeout( 28 | () => { 29 | // regular test for each kind of file + extras 30 | readdir(join(__dirname, 'source'), (_, files) => { 31 | for (const file of files) { 32 | ucompress( 33 | join(__dirname, 'source', file), 34 | join(__dirname, 'dest', file), 35 | {createFiles: true, noMinify: true, noImport: true} 36 | ) 37 | .then(dest => ok(`\`${basename(dest)}\``)) 38 | .catch(err => { 39 | error`\`\`\`${err}\`\`\``; 40 | process.exit(1); 41 | }); 42 | } 43 | }); 44 | setTimeout( 45 | () => { 46 | // regular test for each kind of file + extras + maxWidth 47 | readdir(join(__dirname, 'source'), (_, files) => { 48 | for (const file of files) { 49 | ucompress( 50 | join(__dirname, 'source', file), 51 | join(__dirname, 'dest', file), 52 | { 53 | createFiles: true, 54 | maxWidth: 320, 55 | maxHeight: 320, 56 | preview: true, 57 | sourceMap: true 58 | } 59 | ) 60 | .then(dest => ok(`\`${basename(dest)}\``)) 61 | .catch(err => { 62 | error`\`\`\`${err}\`\`\``; 63 | process.exit(1); 64 | }); 65 | } 66 | }); 67 | setTimeout( 68 | () => { 69 | // wrong source files 70 | readdir(join(__dirname, 'source'), (_, files) => { 71 | for (const file of files) { 72 | ucompress( 73 | join(__dirname, 'source', `no-${file}`), 74 | join(__dirname, 'dest', `no-${file}`) 75 | ) 76 | .then(dest => { 77 | error`\`\`\`${dest}\`\`\` did not exists, this should've failed!`; 78 | process.exit(1); 79 | }) 80 | .catch(() => ok`\`no-${file}\` failed`); 81 | } 82 | }); 83 | setTimeout( 84 | () => { 85 | ucompress.createHeaders(join(__dirname, 'prepared')).then( 86 | () => ok`createHeaders works`, 87 | () => { 88 | error`createHeaders failure`; 89 | process.exit(1); 90 | } 91 | ); 92 | setTimeout( 93 | lastChecks, 94 | TIMEOUT 95 | ); 96 | }, 97 | TIMEOUT 98 | ); 99 | }, 100 | TIMEOUT 101 | ); 102 | }, 103 | TIMEOUT 104 | ); 105 | }, 106 | TIMEOUT 107 | ); 108 | 109 | function lastChecks() { 110 | // wrong destination folders 111 | ucompress.css( 112 | join(__dirname, 'source', 'index.css'), 113 | join(__dirname, 'shenanigans', `index.css`) 114 | ) 115 | .then(dest => { 116 | error`\`\`\`${dest}\`\`\` did not exists, this should've failed!`; 117 | process.exit(1); 118 | }) 119 | .catch(() => ok`wrong CSS destination fails as expected`); 120 | 121 | ucompress.html( 122 | join(__dirname, 'source', 'index.html'), 123 | join(__dirname, 'shenanigans', `index.html`) 124 | ) 125 | .then(dest => { 126 | error`\`\`\`${dest}\`\`\` did not exists, this should've failed!`; 127 | process.exit(1); 128 | }) 129 | .catch(() => ok`wrong HTML destination fails as expected`); 130 | 131 | ucompress.svg( 132 | join(__dirname, 'source', 'benja-dark.svg'), 133 | join(__dirname, 'shenanigans', `benja-dark.svg`) 134 | ) 135 | .then(dest => { 136 | error`\`\`\`${dest}\`\`\` did not exists, this should've failed!`; 137 | process.exit(1); 138 | }) 139 | .catch(() => ok`wrong SVG destination fails as expected`); 140 | 141 | // other kind of failures 142 | ucompress.js( 143 | join(__dirname, 'source', 'favicon.ico'), 144 | join(__dirname, 'dest', `shenanigans.js`) 145 | ) 146 | .then(dest => { 147 | error`\`\`\`${dest}\`\`\` did not exists, this should've failed!`; 148 | process.exit(1); 149 | }) 150 | .catch(() => ok`bad JS fails as expected`); 151 | 152 | ucompress.html( 153 | join(__dirname, 'source', 'favicon.ico'), 154 | join(__dirname, 'dest', `shenanigans.html`) 155 | ) 156 | .then(dest => { 157 | error`\`\`\`${dest}\`\`\` did not exists, this should've failed!`; 158 | process.exit(1); 159 | }) 160 | .catch(() => ok`bad HTML fails as expected`); 161 | 162 | ucompress.png( 163 | join(__dirname, 'source', 'shenanigans.png'), 164 | join(__dirname, 'dest', `shenanigans.png`) 165 | ) 166 | .then(dest => { 167 | error`\`\`\`${dest}\`\`\` did not exists, this should've failed!`; 168 | process.exit(1); 169 | }) 170 | .catch(() => ok`bad PNG fails as expected`); 171 | } 172 | -------------------------------------------------------------------------------- /binary.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {mkdir, readdir, stat} = require('fs'); 4 | const {basename, dirname, extname, join, resolve} = require('path'); 5 | 6 | const umap = require('umap'); 7 | const jsModules = umap(new Map); 8 | 9 | const ucompress = require('./cjs/index.js'); 10 | const blur = require('./cjs/preview.js'); 11 | 12 | let source = '.'; 13 | let dest = ''; 14 | let headers = false; 15 | let help = false; 16 | let noImport = false; 17 | let noMinify = false; 18 | let preview = false; 19 | let sourceMap = false; 20 | let maxWidth, maxHeight; 21 | 22 | for (let {argv} = process, {length} = argv, i = 2; i < length; i++) { 23 | 24 | // utils 25 | const asInt = ({$1}) => parseInt($1 ? $1.slice(1) : argv[++i], 10); 26 | const asString = ({$1}) => ($1 ? $1.slice(1) : argv[++i]); 27 | 28 | switch (true) { 29 | 30 | // integers 31 | case /^--max-width(=\d+)?$/.test(argv[i]): 32 | maxWidth = asInt(RegExp); 33 | break; 34 | case /^--max-height(=\d+)?$/.test(argv[i]): 35 | maxHeight = asInt(RegExp); 36 | break; 37 | 38 | // strings as paths 39 | case /^--dest(=.+)?$/.test(argv[i]): 40 | dest = resolve(process.cwd(), asString(RegExp)); 41 | break; 42 | case /^--source(=.+)?$/.test(argv[i]): 43 | source = resolve(process.cwd(), asString(RegExp)); 44 | break; 45 | 46 | // no value needed 47 | case /^--create-headers$/.test(argv[i]): 48 | headers = true; 49 | break; 50 | case /^--with-source-map$/.test(argv[i]): 51 | case /^--source-map$/.test(argv[i]): 52 | sourceMap = true; 53 | break; 54 | case /^--with-preview$/.test(argv[i]): 55 | case /^--preview$/.test(argv[i]): 56 | preview = true; 57 | break; 58 | case /^--no-imports?$/.test(argv[i]): 59 | noImport = true; 60 | break; 61 | case /^--no-minify$/.test(argv[i]): 62 | noMinify = true; 63 | break; 64 | case /^--help$/.test(argv[i]): 65 | default: 66 | help = true; 67 | i = length; 68 | break; 69 | } 70 | } 71 | 72 | if ((headers || preview) && !dest) 73 | dest = source; 74 | 75 | if (help || !dest) { 76 | console.log(''); 77 | console.log(`\x1b[1mucompress --source ./path/ --dest ./other-path/\x1b[0m`); 78 | console.log(` --dest ./ \x1b[2m# destination folder where files are created\x1b[0m`); 79 | console.log(` --source ./ \x1b[2m# file or folder to compress, default current folder\x1b[0m`); 80 | console.log(` --max-width X \x1b[2m# max images width in pixels\x1b[0m`); 81 | console.log(` --max-height X \x1b[2m# max images height in pixels\x1b[0m`); 82 | console.log(` --create-headers \x1b[2m# creates .json files to serve as headers\x1b[0m`); 83 | console.log(` --with-preview \x1b[2m# enables *.preview.jpeg images\x1b[0m`); 84 | console.log(` --preview \x1b[2m# alias for --with-preview\x1b[0m`); 85 | console.log(` --with-source-map \x1b[2m# generates source map\x1b[0m`); 86 | console.log(` --source-map \x1b[2m# alias for --source-map\x1b[0m`); 87 | console.log(` --no-import \x1b[2m# avoid resolving imports\x1b[0m`); 88 | console.log(` --no-minify \x1b[2m# avoid source code minification\x1b[0m`); 89 | console.log(''); 90 | console.log('\x1b[1m\x1b[2mPlease note:\x1b[0m\x1b[2m if source and dest are the same, both \x1b[0m--create-headers'); 91 | console.log('\x1b[2mand \x1b[0m--with-preview\x1b[2m, or \x1b[0m--preview\x1b[2m, will \x1b[1mnot\x1b[0m\x1b[2m compress any file.\x1b[0m'); 92 | console.log(''); 93 | } 94 | else { 95 | const error = err => { 96 | console.error(err); 97 | process.exit(1); 98 | }; 99 | const samePath = dest === source; 100 | if (headers && samePath) 101 | ucompress.createHeaders(dest).catch(error); 102 | else if (preview && samePath) { 103 | const crawl = source => new Promise((res, rej) => { 104 | stat(source, (err, stat) => { 105 | /* istanbul ignore if */ 106 | if (err) 107 | rej(err); 108 | else { 109 | if (stat.isFile()) 110 | blur(source).then(res, rej); 111 | /* istanbul ignore else */ 112 | else if (stat.isDirectory()) 113 | readdir(source, (err, files) => { 114 | /* istanbul ignore if */ 115 | if (err) 116 | rej(err); 117 | else 118 | Promise.all(files 119 | .filter(file => !/^[._]/.test(file) && 120 | /\.jpe?g$/i.test(file) && 121 | !/\.preview\.jpe?g$/i.test(file)) 122 | .map(file => crawl(join(source, file))) 123 | ).then(res, rej); 124 | }); 125 | } 126 | }); 127 | }); 128 | crawl(source).catch(error); 129 | } 130 | else { 131 | let jsSource = source; 132 | let jsDest = dest; 133 | let checkEntry = true; 134 | const crawl = (source, dest, options) => new Promise((res, rej) => { 135 | stat(source, (err, stat) => { 136 | /* istanbul ignore if */ 137 | if (err) 138 | rej(err); 139 | else { 140 | if (stat.isFile()) { 141 | if (checkEntry) { 142 | checkEntry = false; 143 | jsSource = dirname(jsSource); 144 | jsDest = dirname(jsDest); 145 | } 146 | if (/\.m?js$/i.test(extname(source))) 147 | ucompress.js(source, dest, options, jsModules, jsSource, jsDest) 148 | .then(res, rej); 149 | else 150 | ucompress(source, dest, options) 151 | .then(res, rej); 152 | } 153 | /* istanbul ignore else */ 154 | else if (stat.isDirectory() && basename(source) !== 'node_modules') { 155 | checkEntry = false; 156 | const onDir = err => { 157 | if (err) 158 | rej(err); 159 | else 160 | readdir(source, (err, files) => { 161 | /* istanbul ignore if */ 162 | if (err) 163 | rej(err); 164 | else 165 | Promise.all(files 166 | .filter(file => !/^[._]/.test(file)) 167 | .map(file => crawl(join(source, file), join(dest, file), options)) 168 | ).then(res, rej); 169 | }); 170 | }; 171 | if (samePath) 172 | onDir(null); 173 | else 174 | mkdir(dest, {recursive: true}, onDir); 175 | } 176 | else 177 | res(dest); 178 | } 179 | }); 180 | }); 181 | crawl( 182 | source, 183 | dest, 184 | { 185 | createFiles: headers, 186 | maxWidth, maxHeight, 187 | preview, sourceMap, 188 | noImport, noMinify 189 | } 190 | ) 191 | .catch(error); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # µcompress 2 | 3 | [![Build Status](https://travis-ci.com/WebReflection/ucompress.svg?branch=master)](https://travis-ci.com/WebReflection/ucompress) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/ucompress/badge.svg?branch=master)](https://coveralls.io/github/WebReflection/ucompress?branch=master) 4 | 5 | 6 | ![compressed umbrellas](./test/ucompress.jpg) 7 | 8 | **Social Media Photo by [Kevin Borrill](https://unsplash.com/@kev2480) on [Unsplash](https://unsplash.com/)** 9 | 10 | A micro, all-in-one, compressor for common Web files, resolving automatically _JavaScript_ imports, when these are static. 11 | 12 | 13 | ### 📣 Community Announcement 14 | 15 | Please ask questions in the [dedicated forum](https://webreflection.boards.net/) to help the community around this project grow ♥ 16 | 17 | --- 18 | 19 | ## As CLI 20 | 21 | Due amount of dependencies, it's recommended to install this module via `npm i -g ucompress`. However you can try it via `npx` too. 22 | 23 | ```sh 24 | # either npm i -g ucompress once, or ... 25 | npx ucompress --help 26 | ``` 27 | 28 | If `--source` and `--dest` parameter are passed, it will do everything automatically. 29 | 30 | ```sh 31 | ucompress --source ./src --dest ./public 32 | ``` 33 | 34 | Check other flags for extra optimizations, such as `.json` headers files and/or `.br`, `.gzip`, and `.deflate` versions, which you can serve via NodeJS or Express, using [µcdn-utils](https://github.com/WebReflection/ucdn-utils#readme). 35 | 36 | 37 | 38 | ## As module 39 | 40 | ```js 41 | import ucompress from 'ucompress'; 42 | // const ucompress = require('ucompress'); 43 | 44 | // define or retrieve `source` and `dest as you like 45 | 46 | // automatic extension => compression 47 | ucompress(source, dest).then(dest => console.log(dest)); 48 | 49 | // explicit compression 50 | ucompress.html(source, dest).then(dest => console.log(dest)); 51 | 52 | // handy fallback 53 | ucompress.copy(source, dest).then(dest => console.log(dest)); 54 | ``` 55 | 56 | 57 | ### Options 58 | 59 | The optional third `options` _object_ parameter can contain any of the following properties: 60 | 61 | * `createFile`, a _boolean_ property, `false` by default, that will automatically pre-compress via _brotli_, _gzip_, and _deflate_, compatible files, plus it will create a `.json` file with pre-processed _headers_ details per each file 62 | * `maxWidth`, an _integer_ property, that if provided, it will reduce, if necessary, the destination image _width_ when it comes to _JPG_ or _PNG_ files 63 | * `maxHeight`, an _integer_ property, that if provided, it will reduce, if necessary, the destination image _height_ when it comes to _JPG_ or _PNG_ files 64 | * `preview`, a _boolean_ parameter, false by default, that creates _JPG_ counter `.preview.jpg` files to be served instead of originals, enabling bandwidth saving, especially for very big pictures (example: [with-preview](https://github.com/WebReflection/with-preview/#readme)) 65 | * `noImport`, a _boolean_ parameter, false by default, that skips automatic _ESM_ `import` resolution, in case the site provides imports maps by itself 66 | * `noMinify`, a _boolean_ parameter, false by default, that keeps the `.js`, `.css`, and `.html` source intact, still performing other changes, such as `.js` imports 67 | 68 | 69 | 70 | ## As Micro CDN 71 | 72 | If you'd like to use this module to serve files _CDN_ like, check **[µcdn](https://github.com/WebReflection/ucdn#readme)** out, it includes ucompress already, as [explained in this post](https://medium.com/@WebReflection/%C2%B5compress-goodbye-bundlers-bb66a854fc3c). 73 | 74 | 75 | ### Compressions 76 | 77 | Following the list of tools ued to optimized various files: 78 | 79 | * **css** files via [csso](https://www.npmjs.com/package/csso) 80 | * **gif** files via [gifsicle](https://www.npmjs.com/package/gifsicle) as *optional dependency* 81 | * **html** files via [html-minifier](https://www.npmjs.com/package/html-minifier) 82 | * **jpg** or **jpeg** files via [sharp](https://github.com/lovell/sharp) 83 | * **js** or **mjs** files via [terser](https://github.com/terser/terser) and [html-minifier](https://github.com/kangax/html-minifier) 84 | * **json** files are simply parsed and stringified so that white spaces get removed 85 | * **md** files are transformed into their `.md.preview.html` version, if the _preview_ is enabled, through [marked](https://github.com/markedjs/marked) 86 | * **png** files via [pngquant-bin](https://www.npmjs.com/package/pngquant-bin) 87 | * **svg** files via [svgo](https://www.npmjs.com/package/svgo) 88 | * **xml** files via [html-minifier](https://www.npmjs.com/package/html-minifier) 89 | 90 | 91 | ### About Automatic Modules Resolution 92 | 93 | If your modules are published as [dual-module](https://medium.com/@WebReflection/a-nodejs-dual-module-deep-dive-8f94ff56210e), or if you have a `module` field in your `package.json`, and it points at an _ESM_ compatible file, as it should, or if you have a `type` field equal to `module` and a `main` that points at an _ESM_ compatible, or if you have an `exports` field which `import` resolves to an _ESM_ compatible module, _µcompress_ will resolve that entry point automatically. 94 | 95 | In every other case, the _import_ will be left untouched, eventually warning in console when such _import_ failed. 96 | 97 | **Dynamic imports** are resolved in a very similar way, but composed imports will likely fail: 98 | 99 | ```js 100 | // these work if the module is found in the source path 101 | // as example, inside source/node_modules 102 | import 'module-a'; 103 | import('module-b').then(...); 104 | 105 | // these work *only* if the file is in the source path 106 | // but not within a module, as resolved modules are not 107 | // copied over, only known imports, eventually, are 108 | import(`/js/${strategy}.js`); 109 | 110 | // these will *not* work 111 | import(condition ? 'condition-thing' : 'another-thing'); 112 | import('a' + thing + '.js'); 113 | ``` 114 | 115 | 116 | ### About `ucompress.createHeaders(path[, headers])` 117 | 118 | This method creates headers for a specific file, or all files within a folder, excluding files that starts with a `.` dot, an `_` underscore, or files within a `node_modules` folder (_you know, that hole that should never fully land in production_). 119 | 120 | ```js 121 | ucompress.createHeaders( 122 | // a folder with already optimized files 123 | '/path/static', 124 | // optional headers to set per folder 125 | {'Access-Control-Allow-Origin': '*'} 126 | ); 127 | ``` 128 | 129 | 130 | ### Brotli, Deflate, GZip, and Headers 131 | 132 | If the third, optional object, contains a `{createFile: true}` flag, each file will automatically generate its own related `.json` file which includes a [RFC-7232](https://tools.ietf.org/html/rfc7232#section-2.3.3) compliant _ETag_, among other details such as `last-modified`, `content-type`, and `content-length`. 133 | 134 | The following file extensions, available via the `ucompress.encoded` _Set_, will also create their `.br`, `.deflate`, and `.gzip` version in the destination folder, plus their own `.json` file, per each different compression, but **only** when `{createFile: true}` is passed. 135 | 136 | * **.css** 137 | * **.html** 138 | * **.js** 139 | * **.mjs** 140 | * **.map** 141 | * **.json** 142 | * **.md** 143 | * **.svg** 144 | * **.txt** 145 | * **.woff2** 146 | * **.xml** 147 | * **.yml** 148 | 149 | Incompatible files will fallback as regular copy `source` into `dest` when the module is used as callback, without creating any optimized version, still providing headers when the flag is used. 150 | -------------------------------------------------------------------------------- /test/source/benja-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 24 | 28 | 32 | 36 | 40 | 44 | 45 | 54 | 56 | 60 | 64 | 65 | 74 | 75 | 77 | 78 | 80 | image/svg+xml 81 | 83 | 84 | 85 | 86 | 87 | 90 | 98 | 106 | 110 | 111 | 115 | 118 | 122 | 126 | 127 | 128 | 129 | 132 | 136 | 140 | 144 | 145 | 146 | 147 | 151 | 155 | 160 | 165 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /esm/js.js: -------------------------------------------------------------------------------- 1 | import {mkdir, readFile, writeFile} from 'fs'; 2 | import {platform} from 'os'; 3 | import {basename, dirname, join, relative, resolve} from 'path'; 4 | 5 | import * as mhl from 'minify-html-literals'; 6 | import {minify as terserMinify} from 'terser'; 7 | import umap from 'umap'; 8 | import umeta from 'umeta'; 9 | 10 | import compressed from './compressed.js'; 11 | import compress from './compress.js'; 12 | import minifyOptions from './html-minifier.js'; 13 | 14 | const {parse, stringify} = JSON; 15 | const {minifyHTMLLiterals} = (mhl.default || mhl); 16 | 17 | const {require: $require} = umeta(import.meta); 18 | const isWindows = /^win/i.test(platform()); 19 | const terserArgs = {output: {comments: /^!/}}; 20 | 21 | compressed.add('.js'); 22 | compressed.add('.mjs'); 23 | compressed.add('.map'); 24 | 25 | const minify = (source, {noMinify, sourceMap}) => new Promise((res, rej) => { 26 | readFile(source, (err, data) => { 27 | if (err) 28 | rej(err); 29 | else { 30 | const original = data.toString(); 31 | /* istanbul ignore if */ 32 | if (noMinify) 33 | res({original, code: original, map: ''}); 34 | else { 35 | try { 36 | const mini = sourceMap ? 37 | // TODO: find a way to integrate literals minification 38 | {code: original} : 39 | minifyHTMLLiterals(original, {minifyOptions}); 40 | /* istanbul ignore next */ 41 | const js = mini ? mini.code : original; 42 | const module = /\.mjs$/.test(source) || 43 | /\b(?:import|export)\b/.test(js); 44 | terserMinify( 45 | js, 46 | sourceMap ? 47 | { 48 | ...terserArgs, 49 | module, 50 | sourceMap: { 51 | filename: source, 52 | url: `${source}.map` 53 | } 54 | } : 55 | { 56 | ...terserArgs, 57 | module 58 | } 59 | ) 60 | .then(({code, map}) => { 61 | res({original, code, map: sourceMap ? map : ''}); 62 | }) 63 | .catch(rej); 64 | } 65 | catch (error) { 66 | /* istanbul ignore next */ 67 | rej(error); 68 | } 69 | } 70 | } 71 | }); 72 | }); 73 | 74 | const uModules = path => path.replace(/\bnode_modules\b/g, 'u_modules'); 75 | 76 | /* istanbul ignore next */ 77 | const noBackSlashes = s => (isWindows ? s.replace(/\\(?!\s)/g, '/') : s); 78 | 79 | const saveCode = (source, dest, code, options) => 80 | new Promise((res, rej) => { 81 | dest = uModules(dest); 82 | mkdir(dirname(dest), {recursive: true}, err => { 83 | /* istanbul ignore if */ 84 | if (err) 85 | rej(err); 86 | else { 87 | writeFile(dest, code, err => { 88 | /* istanbul ignore if */ 89 | if (err) 90 | rej(err); 91 | else if (options.createFiles) 92 | compress(source, dest, 'text', options) 93 | .then(() => res(dest), rej); 94 | else 95 | res(dest); 96 | }); 97 | } 98 | }); 99 | }); 100 | 101 | /** 102 | * Create a file after minifying it via `uglify-es`. 103 | * @param {string} source The source JS file to minify. 104 | * @param {string} dest The minified destination file. 105 | * @param {Options} [options] Options to deal with extra computation. 106 | * @return {Promise} A promise that resolves with the destination file. 107 | */ 108 | const JS = ( 109 | source, dest, options = {}, 110 | /* istanbul ignore next */ known = umap(new Map), 111 | initialSource = dirname(source), 112 | initialDest = dirname(dest) 113 | ) => known.get(dest) || known.set(dest, minify(source, options).then( 114 | ({original, code, map}) => { 115 | const modules = []; 116 | const newCode = []; 117 | if (options.noImport) 118 | newCode.push(code); 119 | else { 120 | const baseSource = dirname(source); 121 | const baseDest = dirname(dest); 122 | const re = /(["'`])(?:(?=(\\?))\2.)*?\1/g; 123 | let i = 0, match; 124 | while (match = re.exec(code)) { 125 | const {0: whole, 1: quote, index} = match; 126 | const chunk = code.slice(i, index); 127 | const next = index + whole.length; 128 | let content = whole; 129 | newCode.push(chunk); 130 | /* istanbul ignore else */ 131 | if ( 132 | /(?:\bfrom\b|\bimport\b\(?)\s*$/.test(chunk) && 133 | (!/\(\s*$/.test(chunk) || /^\s*\)/.test(code.slice(next))) 134 | ) { 135 | const module = whole.slice(1, -1); 136 | if (/^[a-z@][a-z0-9/._-]+$/i.test(module)) { 137 | try { 138 | const {length} = module; 139 | let path = $require.resolve(module, {paths: [baseSource]}); 140 | /* istanbul ignore next */ 141 | if (!path.includes(module) && /(\/|\\[^ ])/.test(module)) { 142 | const sep = RegExp.$1[0]; 143 | const source = module.split(sep); 144 | const target = path.split(sep); 145 | const js = source.length; 146 | for (let j = 0, i = target.indexOf(source[0]); i < target.length; i++) { 147 | if (j < js && target[i] !== source[j++]) 148 | target[i] = source[j - 1]; 149 | } 150 | path = target.join(sep); 151 | path = [ 152 | path.slice(0, path.lastIndexOf('node_modules')), 153 | 'node_modules', 154 | source[0] 155 | ].join(sep); 156 | } 157 | else { 158 | let oldPath = path; 159 | do path = dirname(oldPath); 160 | while ( 161 | path !== oldPath && 162 | path.slice(-length) !== module && 163 | (oldPath = path) 164 | ); 165 | } 166 | const i = path.lastIndexOf('node_modules'); 167 | /* istanbul ignore if */ 168 | if (i < 0) 169 | throw new Error('node_modules folder not found'); 170 | const {exports: e, module: m, main, type} = $require( 171 | join(path, 'package.json') 172 | ); 173 | /* istanbul ignore next */ 174 | const index = (e && (e.import || e['.'].import)) || m || (type === 'module' && main); 175 | /* istanbul ignore if */ 176 | if (!index) 177 | throw new Error('no entry file found'); 178 | const newSource = resolve(path, index); 179 | const newDest = resolve(initialDest, path.slice(i), index); 180 | modules.push(JS( 181 | newSource, newDest, 182 | options, known, 183 | initialSource, initialDest 184 | )); 185 | path = uModules( 186 | noBackSlashes(relative(dirname(source), newSource)) 187 | ); 188 | /* istanbul ignore next */ 189 | content = `${quote}${path[0] === '.' ? path : `./${path}`}${quote}`; 190 | } 191 | catch ({message}) { 192 | console.warn(`unable to import "${module}"`, message); 193 | } 194 | } 195 | /* istanbul ignore else */ 196 | else if (!/^([a-z]+:)?\/\//i.test(module)) { 197 | modules.push(JS( 198 | resolve(baseSource, module), 199 | resolve(baseDest, module), 200 | options, known, 201 | initialSource, initialDest 202 | )); 203 | } 204 | } 205 | newCode.push(content); 206 | i = next; 207 | } 208 | newCode.push(code.slice(i)); 209 | } 210 | let smCode = newCode.join(''); 211 | if (options.sourceMap) { 212 | const destSource = dest.replace(/(\.m?js)$/i, `$1.source$1`); 213 | const json = parse(map); 214 | const file = basename(dest); 215 | json.file = file; 216 | json.sources = [basename(destSource)]; 217 | smCode = smCode.replace(source, file); 218 | modules.push( 219 | saveCode(source, `${dest}.map`, stringify(json), options), 220 | saveCode(source, destSource, original, options) 221 | ); 222 | } 223 | return Promise.all( 224 | modules.concat(saveCode(source, dest, smCode, options)) 225 | ).then( 226 | () => dest, 227 | /* istanbul ignore next */ 228 | err => Promise.reject(err) 229 | ); 230 | } 231 | )); 232 | 233 | export default JS; 234 | -------------------------------------------------------------------------------- /cjs/js.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {mkdir, readFile, writeFile} = require('fs'); 3 | const {platform} = require('os'); 4 | const {basename, dirname, join, relative, resolve} = require('path'); 5 | 6 | const mhl = require('minify-html-literals'); 7 | const {minify: terserMinify} = require('terser'); 8 | const umap = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('umap')); 9 | const umeta = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('umeta')); 10 | 11 | const compressed = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compressed.js')); 12 | const compress = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./compress.js')); 13 | const minifyOptions = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./html-minifier.js')); 14 | 15 | const {parse, stringify} = JSON; 16 | const {minifyHTMLLiterals} = (mhl.default || mhl); 17 | 18 | const {require: $require} = umeta(({url: require('url').pathToFileURL(__filename).href})); 19 | const isWindows = /^win/i.test(platform()); 20 | const terserArgs = {output: {comments: /^!/}}; 21 | 22 | compressed.add('.js'); 23 | compressed.add('.mjs'); 24 | compressed.add('.map'); 25 | 26 | const minify = (source, {noMinify, sourceMap}) => new Promise((res, rej) => { 27 | readFile(source, (err, data) => { 28 | if (err) 29 | rej(err); 30 | else { 31 | const original = data.toString(); 32 | /* istanbul ignore if */ 33 | if (noMinify) 34 | res({original, code: original, map: ''}); 35 | else { 36 | try { 37 | const mini = sourceMap ? 38 | // TODO: find a way to integrate literals minification 39 | {code: original} : 40 | minifyHTMLLiterals(original, {minifyOptions}); 41 | /* istanbul ignore next */ 42 | const js = mini ? mini.code : original; 43 | const module = /\.mjs$/.test(source) || 44 | /\b(?:import|export)\b/.test(js); 45 | terserMinify( 46 | js, 47 | sourceMap ? 48 | { 49 | ...terserArgs, 50 | module, 51 | sourceMap: { 52 | filename: source, 53 | url: `${source}.map` 54 | } 55 | } : 56 | { 57 | ...terserArgs, 58 | module 59 | } 60 | ) 61 | .then(({code, map}) => { 62 | res({original, code, map: sourceMap ? map : ''}); 63 | }) 64 | .catch(rej); 65 | } 66 | catch (error) { 67 | /* istanbul ignore next */ 68 | rej(error); 69 | } 70 | } 71 | } 72 | }); 73 | }); 74 | 75 | const uModules = path => path.replace(/\bnode_modules\b/g, 'u_modules'); 76 | 77 | /* istanbul ignore next */ 78 | const noBackSlashes = s => (isWindows ? s.replace(/\\(?!\s)/g, '/') : s); 79 | 80 | const saveCode = (source, dest, code, options) => 81 | new Promise((res, rej) => { 82 | dest = uModules(dest); 83 | mkdir(dirname(dest), {recursive: true}, err => { 84 | /* istanbul ignore if */ 85 | if (err) 86 | rej(err); 87 | else { 88 | writeFile(dest, code, err => { 89 | /* istanbul ignore if */ 90 | if (err) 91 | rej(err); 92 | else if (options.createFiles) 93 | compress(source, dest, 'text', options) 94 | .then(() => res(dest), rej); 95 | else 96 | res(dest); 97 | }); 98 | } 99 | }); 100 | }); 101 | 102 | /** 103 | * Create a file after minifying it via `uglify-es`. 104 | * @param {string} source The source JS file to minify. 105 | * @param {string} dest The minified destination file. 106 | * @param {Options} [options] Options to deal with extra computation. 107 | * @return {Promise} A promise that resolves with the destination file. 108 | */ 109 | const JS = ( 110 | source, dest, options = {}, 111 | /* istanbul ignore next */ known = umap(new Map), 112 | initialSource = dirname(source), 113 | initialDest = dirname(dest) 114 | ) => known.get(dest) || known.set(dest, minify(source, options).then( 115 | ({original, code, map}) => { 116 | const modules = []; 117 | const newCode = []; 118 | if (options.noImport) 119 | newCode.push(code); 120 | else { 121 | const baseSource = dirname(source); 122 | const baseDest = dirname(dest); 123 | const re = /(["'`])(?:(?=(\\?))\2.)*?\1/g; 124 | let i = 0, match; 125 | while (match = re.exec(code)) { 126 | const {0: whole, 1: quote, index} = match; 127 | const chunk = code.slice(i, index); 128 | const next = index + whole.length; 129 | let content = whole; 130 | newCode.push(chunk); 131 | /* istanbul ignore else */ 132 | if ( 133 | /(?:\bfrom\b|\bimport\b\(?)\s*$/.test(chunk) && 134 | (!/\(\s*$/.test(chunk) || /^\s*\)/.test(code.slice(next))) 135 | ) { 136 | const module = whole.slice(1, -1); 137 | if (/^[a-z@][a-z0-9/._-]+$/i.test(module)) { 138 | try { 139 | const {length} = module; 140 | let path = $require.resolve(module, {paths: [baseSource]}); 141 | /* istanbul ignore next */ 142 | if (!path.includes(module) && /(\/|\\[^ ])/.test(module)) { 143 | const sep = RegExp.$1[0]; 144 | const source = module.split(sep); 145 | const target = path.split(sep); 146 | const js = source.length; 147 | for (let j = 0, i = target.indexOf(source[0]); i < target.length; i++) { 148 | if (j < js && target[i] !== source[j++]) 149 | target[i] = source[j - 1]; 150 | } 151 | path = target.join(sep); 152 | path = [ 153 | path.slice(0, path.lastIndexOf('node_modules')), 154 | 'node_modules', 155 | source[0] 156 | ].join(sep); 157 | } 158 | else { 159 | let oldPath = path; 160 | do path = dirname(oldPath); 161 | while ( 162 | path !== oldPath && 163 | path.slice(-length) !== module && 164 | (oldPath = path) 165 | ); 166 | } 167 | const i = path.lastIndexOf('node_modules'); 168 | /* istanbul ignore if */ 169 | if (i < 0) 170 | throw new Error('node_modules folder not found'); 171 | const {exports: e, module: m, main, type} = $require( 172 | join(path, 'package.json') 173 | ); 174 | /* istanbul ignore next */ 175 | const index = (e && (e.import || e['.'].import)) || m || (type === 'module' && main); 176 | /* istanbul ignore if */ 177 | if (!index) 178 | throw new Error('no entry file found'); 179 | const newSource = resolve(path, index); 180 | const newDest = resolve(initialDest, path.slice(i), index); 181 | modules.push(JS( 182 | newSource, newDest, 183 | options, known, 184 | initialSource, initialDest 185 | )); 186 | path = uModules( 187 | noBackSlashes(relative(dirname(source), newSource)) 188 | ); 189 | /* istanbul ignore next */ 190 | content = `${quote}${path[0] === '.' ? path : `./${path}`}${quote}`; 191 | } 192 | catch ({message}) { 193 | console.warn(`unable to import "${module}"`, message); 194 | } 195 | } 196 | /* istanbul ignore else */ 197 | else if (!/^([a-z]+:)?\/\//i.test(module)) { 198 | modules.push(JS( 199 | resolve(baseSource, module), 200 | resolve(baseDest, module), 201 | options, known, 202 | initialSource, initialDest 203 | )); 204 | } 205 | } 206 | newCode.push(content); 207 | i = next; 208 | } 209 | newCode.push(code.slice(i)); 210 | } 211 | let smCode = newCode.join(''); 212 | if (options.sourceMap) { 213 | const destSource = dest.replace(/(\.m?js)$/i, `$1.source$1`); 214 | const json = parse(map); 215 | const file = basename(dest); 216 | json.file = file; 217 | json.sources = [basename(destSource)]; 218 | smCode = smCode.replace(source, file); 219 | modules.push( 220 | saveCode(source, `${dest}.map`, stringify(json), options), 221 | saveCode(source, destSource, original, options) 222 | ); 223 | } 224 | return Promise.all( 225 | modules.concat(saveCode(source, dest, smCode, options)) 226 | ).then( 227 | () => dest, 228 | /* istanbul ignore next */ 229 | err => Promise.reject(err) 230 | ); 231 | } 232 | )); 233 | 234 | module.exports = JS; 235 | --------------------------------------------------------------------------------