/* @flow */
import type {ProgramData, ProgramDataCallback, CompilerConfig} from './typedef';
import mkdirp from 'mkdirp';
import {dirname} from 'path';
import {writeFile, readFile} from 'fs';
import {gzip, gunzip} from 'zlib';
import {isProduction} from './util';
import {logError, logSequentialSuccessMessage} from './logger';
const defaultOptions = {
compress: isProduction
};
/**
* The base compiler class
*
* @class Compiler
* @abstract
* @protected
* @param {CompilerConfig} [options={}] - configuration object
*/
export class Compiler {
/**
* configured options
*
* @member {CompilerConfig} options
* @memberof Compiler
* @private
* @instance
*/
options: Object;
// eslint-disable-next-line require-jsdoc
constructor(options: CompilerConfig = {}) {
this.options = {...defaultOptions, ...options};
}
/**
* Executed when the compilation is complete
*
* @memberOf Compiler
* @static
* @method done
* @param {string} inPath - the input path
* @param {Function} callback - a callback function
*/
static done(inPath: string, callback: () => void) {
logSequentialSuccessMessage(`Compiled ${inPath}`);
callback();
}
/**
* Writes the data to disk and then calls `done`.
*
* @memberOf Compiler
* @static
* @private
* @method writeAndCallDone
* @param {string} inPath - the input path
* @param {string} outPath - the output path
* @param {ProgramData} data - processed application code with source maps
* @param {Function} callback - a callback function
*/
static writeAndCallDone(inPath: string, outPath: string, data: ProgramData, callback: () => void) {
Compiler.fsWrite(outPath, data, () => {
Compiler.done(inPath, callback);
});
}
/**
* Writes the data to disk
*
* @memberOf Compiler
* @static
* @method fsWrite
* @param {string} path - the output path
* @param {ProgramData} data - the data to write
* @param {Function} callback - a callback function
* @return {void}
*/
static fsWrite(path: string, data: ProgramData, callback: () => void) {
if (!data.code) {
return callback();
}
Compiler.mkdir(path, () => {
writeFile(path, data.code, scriptErr => {
if (scriptErr) {
return logError(scriptErr);
}
if (!data.map) {
return callback();
}
writeFile(`${path}.map`, data.map, mapErr => {
if (mapErr) {
return logError(mapErr);
}
callback();
});
});
});
}
/**
* Recursively creates a directory containing a file specified by `path`.
*
* @memberOf Compiler
* @static
* @method mkdir
* @param {string} path - a path to a file
* @param {Function} callback - a callback function
*/
static mkdir(path: string, callback: () => void) {
mkdirp(dirname(path), mkdirpErr => {
if (mkdirpErr) {
return logError(mkdirpErr);
}
callback();
});
}
/**
* G-zips the compiled code
*
* @memberOf Compiler
* @static
* @method gzip
* @param {ProgramData} data - the actual program data to gzip
* @param {ProgramDataCallback} callback - a callback function
* @return {void}
*/
static gzip(data: ProgramData, callback: ProgramDataCallback) {
if (!data.code) {
return callback(data);
}
gzip(data.code, (err, code) => {
if (err) {
return logError(err);
}
callback({code, map: data.map});
});
}
/**
* Reads the data from disk
*
* @memberOf Compiler
* @private
* @method fsRead
* @param {string} path - the input path
* @param {ProgramDataCallback} callback - a callback function
*/
fsRead(path: string, callback: ProgramDataCallback) {
readFile(path, (scriptErr, scriptData) => {
if (scriptErr) {
return callback({code: '', map: ''});
}
readFile(`${path}.map`, 'utf8', (mapErr, mapData) => {
const map = mapErr ? '' : mapData;
if (!this.options.compress) {
return callback({code: scriptData.toString('utf8'), map});
}
gunzip(scriptData, (zipErr, zipData) => {
callback({code: zipErr ? '' : zipData.toString('utf8'), map});
});
});
});
}
/**
* G-zips the program if necessary and writes the results to disk.
*
* Skips the final write if the contents of the file have not changed since the previous write.
* Which adds a little overhead at compile time, but at the same time does not alter the last modified timestamp of
* the file unnecessarily.
*
* Good news for someone who is using that timestamp for public cache invalidation.
*
* @memberOf Compiler
* @instance
* @protected
* @method save
* @param {string} inPath - the input path
* @param {string} outPath - the output path
* @param {ProgramData} data - processed application code with source maps
* @param {Function} callback - a callback function
*/
save(inPath: string, outPath: string, data: ProgramData, callback: () => void) {
this.fsRead(outPath, oldData => {
const newData = {
code: oldData.code === data.code ? '' : data.code,
map: oldData.map === data.map ? '' : data.map
};
if (!this.options.compress) {
Compiler.writeAndCallDone(inPath, outPath, newData, callback);
return;
}
Compiler.gzip(newData, result => {
Compiler.writeAndCallDone(inPath, outPath, result, callback);
});
});
}
}