2021-10-13 15:05:52 +03:00
|
|
|
const errors = require('@tryghost/errors');
|
2021-10-19 15:05:23 +03:00
|
|
|
const debug = require('@tryghost/debug')('minifier');
|
2021-10-13 15:05:52 +03:00
|
|
|
const tpl = require('@tryghost/tpl');
|
|
|
|
const csso = require('csso');
|
2021-10-19 15:05:23 +03:00
|
|
|
const terser = require('terser');
|
2021-10-13 15:05:52 +03:00
|
|
|
const glob = require('tiny-glob');
|
|
|
|
const path = require('path');
|
|
|
|
const fs = require('fs').promises;
|
|
|
|
|
|
|
|
const messages = {
|
|
|
|
badDestination: {
|
|
|
|
message: 'Unexpected destination {dest}',
|
|
|
|
context: 'Minifier expected a destination that ended in .css or .js'
|
|
|
|
},
|
2021-11-03 17:14:23 +03:00
|
|
|
badSource: {
|
|
|
|
message: 'Unable to read source files {src}',
|
|
|
|
context: 'Minifier was unable to locate or read the source files'
|
|
|
|
},
|
2021-10-13 15:05:52 +03:00
|
|
|
missingConstructorOption: {
|
|
|
|
message: 'Minifier missing {opt} option',
|
|
|
|
context: 'new Minifier({}) requires a {opt} option'
|
|
|
|
},
|
|
|
|
globalHelp: 'Refer to the readme for @tryghost/minifier for how to use this module'
|
|
|
|
};
|
|
|
|
|
|
|
|
// public API for minify hooks
|
|
|
|
class Minifier {
|
|
|
|
constructor({src, dest}) {
|
|
|
|
if (!src) {
|
|
|
|
throw new errors.IncorrectUsageError({
|
|
|
|
message: tpl(messages.missingConstructorOption.message, {opt: 'src'}),
|
|
|
|
context: tpl(messages.missingConstructorOption.context, {opt: 'src'}),
|
|
|
|
help: tpl(messages.globalHelp)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (!dest) {
|
|
|
|
throw new errors.IncorrectUsageError({
|
|
|
|
message: tpl(messages.missingConstructorOption.message, {opt: 'dest'}),
|
|
|
|
context: tpl(messages.missingConstructorOption.context, {opt: 'dest'}),
|
|
|
|
help: tpl(messages.globalHelp)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
this.srcPath = src;
|
|
|
|
this.destPath = dest;
|
|
|
|
}
|
|
|
|
|
|
|
|
getFullSrc(src) {
|
|
|
|
return path.join(this.srcPath, src);
|
|
|
|
}
|
|
|
|
|
|
|
|
getFullDest(dest) {
|
|
|
|
return path.join(this.destPath, dest);
|
|
|
|
}
|
|
|
|
|
|
|
|
async minifyCSS(contents) {
|
2021-10-19 15:05:23 +03:00
|
|
|
const result = await csso.minify(contents);
|
2021-10-13 15:05:52 +03:00
|
|
|
if (result && result.css) {
|
|
|
|
return result.css;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
async minifyJS(contents) {
|
2021-10-19 15:05:23 +03:00
|
|
|
const result = await terser.minify(contents);
|
2021-10-13 15:05:52 +03:00
|
|
|
if (result && result.code) {
|
|
|
|
return result.code;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getMatchingFiles(src) {
|
|
|
|
return await glob(this.getFullSrc(src));
|
|
|
|
}
|
|
|
|
|
|
|
|
async readFiles(files) {
|
|
|
|
let mergedFiles = '';
|
|
|
|
for (const file of files) {
|
|
|
|
const contents = await fs.readFile(file, 'utf8');
|
|
|
|
mergedFiles += contents;
|
|
|
|
}
|
|
|
|
|
|
|
|
return mergedFiles;
|
|
|
|
}
|
|
|
|
|
2021-11-03 17:14:23 +03:00
|
|
|
async getSrcFileContents(src) {
|
|
|
|
try {
|
|
|
|
const files = await this.getMatchingFiles(src);
|
|
|
|
|
|
|
|
if (files) {
|
|
|
|
return await this.readFiles(files);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
throw new errors.IncorrectUsageError({
|
|
|
|
message: tpl(messages.badSource.message, {src}),
|
|
|
|
context: tpl(messages.badSource.context),
|
|
|
|
help: tpl(messages.globalHelp)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-13 15:05:52 +03:00
|
|
|
async writeFile(contents, dest) {
|
|
|
|
if (contents) {
|
|
|
|
let writePath = this.getFullDest(dest);
|
2021-11-19 15:49:41 +03:00
|
|
|
// Ensure the output folder exists
|
|
|
|
await fs.mkdir(this.destPath, {recursive: true});
|
|
|
|
// Create the file
|
2021-10-13 15:05:52 +03:00
|
|
|
await fs.writeFile(writePath, contents);
|
|
|
|
return writePath;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-03 16:59:08 +03:00
|
|
|
/**
|
|
|
|
* Minify files
|
|
|
|
*
|
|
|
|
* @param {Object} globs An object in the form of
|
|
|
|
* ```js
|
|
|
|
* {
|
|
|
|
* 'destination1.js': 'glob/*.js',
|
|
|
|
* 'destination2.js': 'glob2/*.js'
|
|
|
|
* }
|
|
|
|
* ```
|
|
|
|
* @param {Object} [options]
|
|
|
|
* @param {Object} [options.replacements] Key value pairs that should get replaced in the content before minifying
|
|
|
|
* @returns {Promise<string[]>} List of minified files (keys of globs)
|
|
|
|
*/
|
|
|
|
async minify(globs, options) {
|
|
|
|
debug('Begin', globs);
|
|
|
|
const destinations = Object.keys(globs);
|
2021-10-13 15:05:52 +03:00
|
|
|
const minifiedFiles = [];
|
|
|
|
|
|
|
|
for (const dest of destinations) {
|
2022-08-03 16:59:08 +03:00
|
|
|
const src = globs[dest];
|
|
|
|
let contents = await this.getSrcFileContents(src);
|
|
|
|
|
|
|
|
if (options?.replacements) {
|
|
|
|
for (const key of Object.keys(options.replacements)) {
|
|
|
|
contents = contents.replace(key, options.replacements[key]);
|
|
|
|
}
|
|
|
|
}
|
2021-10-13 15:05:52 +03:00
|
|
|
let minifiedContents;
|
|
|
|
|
|
|
|
if (dest.endsWith('.css')) {
|
|
|
|
minifiedContents = await this.minifyCSS(contents);
|
|
|
|
} else if (dest.endsWith('.js')) {
|
|
|
|
minifiedContents = await this.minifyJS(contents);
|
|
|
|
} else {
|
|
|
|
throw new errors.IncorrectUsageError({
|
|
|
|
message: tpl(messages.badDestination.message, {dest}),
|
|
|
|
context: tpl(messages.badDestination.context),
|
|
|
|
help: tpl(messages.globalHelp)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = await this.writeFile(minifiedContents, dest);
|
|
|
|
if (result) {
|
2021-10-19 15:05:23 +03:00
|
|
|
minifiedFiles.push(dest);
|
2021-10-13 15:05:52 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
debug('End');
|
|
|
|
return minifiedFiles;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Minifier;
|