104f84f252
As discussed with the product team we want to enforce kebab-case file names for all files, with the exception of files which export a single class, in which case they should be PascalCase and reflect the class which they export. This will help find classes faster, and should push better naming for them too. Some files and packages have been excluded from this linting, specifically when a library or framework depends on the naming of a file for the functionality e.g. Ember, knex-migrator, adapter-manager
166 lines
5.0 KiB
JavaScript
166 lines
5.0 KiB
JavaScript
const errors = require('@tryghost/errors');
|
|
const debug = require('@tryghost/debug')('minifier');
|
|
const tpl = require('@tryghost/tpl');
|
|
const csso = require('csso');
|
|
const terser = require('terser');
|
|
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'
|
|
},
|
|
badSource: {
|
|
message: 'Unable to read source files {src}',
|
|
context: 'Minifier was unable to locate or read the source files'
|
|
},
|
|
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) {
|
|
const result = await csso.minify(contents);
|
|
if (result && result.css) {
|
|
return result.css;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async minifyJS(contents) {
|
|
const result = await terser.minify(contents);
|
|
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;
|
|
}
|
|
|
|
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)
|
|
});
|
|
}
|
|
}
|
|
|
|
async writeFile(contents, dest) {
|
|
if (contents) {
|
|
let writePath = this.getFullDest(dest);
|
|
// Ensure the output folder exists
|
|
await fs.mkdir(this.destPath, {recursive: true});
|
|
// Create the file
|
|
await fs.writeFile(writePath, contents);
|
|
return writePath;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
const minifiedFiles = [];
|
|
|
|
for (const dest of destinations) {
|
|
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]);
|
|
}
|
|
}
|
|
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) {
|
|
minifiedFiles.push(dest);
|
|
}
|
|
}
|
|
|
|
debug('End');
|
|
return minifiedFiles;
|
|
}
|
|
}
|
|
|
|
module.exports = Minifier;
|