├── 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 | [](https://travis-ci.com/WebReflection/ucompress) [](https://coveralls.io/github/WebReflection/ucompress?branch=master)
4 |
5 |
6 | 
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 |
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 |
--------------------------------------------------------------------------------