mirror of https://github.com/vizality/vizality
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
252 lines
6.4 KiB
252 lines
6.4 KiB
const { readFileSync, writeFileSync, existsSync, mkdirSync } = require('fs');
|
|
const { createHash } = require('crypto');
|
|
const { debounce } = require('lodash');
|
|
const { watch } = require('chokidar');
|
|
const Events = require('events');
|
|
const { join } = require('path');
|
|
|
|
|
|
// @todo: Schedule a cache cleanup?
|
|
|
|
/**
|
|
* Main class for compilers used in Vizality.
|
|
* If using the watcher, MAKE SURE TO DISPOSE OF THE COMPILER PROPERLY. You **MUST** disable
|
|
* the watcher if you no longer need the compiler. When watch events are emitted, the compiler
|
|
* should be re-used if a recompile is performed.
|
|
* @property {string} file File to compile
|
|
* @property {string} cacheDir Path where cached files will go
|
|
* @property {string} watcherEnabled Whether the file watcher is enabled or not
|
|
* @abstract
|
|
*/
|
|
module.exports = class Compiler extends Events {
|
|
constructor (file) {
|
|
super();
|
|
this.file = file;
|
|
this.cacheDir = join(__dirname, '..', '..', '..', '.cache', this.constructor.name.toLowerCase());
|
|
this.watcherEnabled = false;
|
|
this._watchers = {};
|
|
this._compiledOnce = {};
|
|
this._labels = [ 'Compiler', this.constructor.name ];
|
|
if (!existsSync(this.cacheDir)) {
|
|
mkdirSync(this.cacheDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enables the file watcher. Will emit "src-update" event if any of the files are updated.
|
|
*/
|
|
enableWatcher () {
|
|
this.watcherEnabled = true;
|
|
}
|
|
|
|
/**
|
|
* Disables the file watcher. MUST be called if you no longer need the compiler and the watcher
|
|
* was previously enabled.
|
|
*/
|
|
disableWatcher () {
|
|
this.watcherEnabled = false;
|
|
Object.values(this._watchers).forEach(w => w.close());
|
|
this._watchers = {};
|
|
}
|
|
|
|
/**
|
|
* Compiles the file (if necessary), and perform cache-related operations.
|
|
* @returns {Promise<string>|string} Compilation result
|
|
*/
|
|
compile () {
|
|
try {
|
|
// Attemt to fetch from cache
|
|
const cacheKey = this.computeCacheKey();
|
|
if (cacheKey instanceof Promise) {
|
|
return cacheKey.then(key => this._doCompilation(key));
|
|
}
|
|
return this._doCompilation(cacheKey);
|
|
} catch (err) {
|
|
return console.log('pizza', err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_doCompilation (cacheKey) {
|
|
try {
|
|
let cacheFile;
|
|
if (cacheKey) {
|
|
cacheFile = join(this.cacheDir, cacheKey);
|
|
if (existsSync(cacheFile)) {
|
|
const compiled = readFileSync(cacheFile, 'utf8');
|
|
this._finishCompilation(null, compiled);
|
|
return compiled;
|
|
}
|
|
}
|
|
/*
|
|
* Perform compilation.
|
|
*/
|
|
const compiled = this._compile();
|
|
if (compiled instanceof Promise) {
|
|
return compiled.then(finalCompiled => {
|
|
this._finishCompilation(cacheFile, finalCompiled);
|
|
return finalCompiled;
|
|
});
|
|
}
|
|
this._finishCompilation(cacheFile, compiled);
|
|
return compiled;
|
|
} catch (err) {
|
|
return this._error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_finishCompilation (cacheFile, compiled) {
|
|
try {
|
|
if (cacheFile) {
|
|
writeFileSync(cacheFile, compiled, () => void 0);
|
|
}
|
|
if (this.watcherEnabled) {
|
|
this._watchFiles();
|
|
}
|
|
} catch (err) {
|
|
// Triggered when you delete cache (on startup only maybe)
|
|
return console.log('fishing');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _watchFiles () {
|
|
try {
|
|
const files = await this.listFiles();
|
|
/*
|
|
* Filter no longer used watchers.
|
|
*/
|
|
Object.keys(this._watchers).forEach(file => {
|
|
if (!files.includes(file)) {
|
|
this._watchers[file].close();
|
|
delete this._watchers[file];
|
|
}
|
|
});
|
|
/*
|
|
* Add new watchers.
|
|
*/
|
|
files.forEach(file => {
|
|
if (!this._watchers[file]) {
|
|
this._watchers[file] = watch(file);
|
|
this._watchers[file].on('all', debounce(async () => this.emit('src-update'), 300));
|
|
}
|
|
});
|
|
} catch (err) {
|
|
return console.log('fishing');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lists all files involved during the compilation (parent file + imported files).
|
|
* Only applicable if files are concatenated during compilation (e.g. scss files).
|
|
* @returns {Promise<string[]>|string[]}
|
|
*/
|
|
listFiles () {
|
|
try {
|
|
return [ this.file ];
|
|
} catch (err) {
|
|
return console.log('fishing');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computes the hash corresponding to the file we're compiling.
|
|
* MUST take into account imported files (if any) and always return the same hash for the same given file.
|
|
* @returns {Promise<string|null>|string|null} Cache key, or null if cache isn't available
|
|
*/
|
|
computeCacheKey () {
|
|
try {
|
|
const files = this.listFiles();
|
|
if (files instanceof Promise) {
|
|
return files.then(this._computeCacheKey.bind(this));
|
|
}
|
|
return this._computeCacheKey(files);
|
|
} catch (err) {
|
|
return console.log('fishing');
|
|
}
|
|
}
|
|
|
|
/** @private */
|
|
_computeCacheKey (files) {
|
|
try {
|
|
const hashes = files.map(this.computeFileHash.bind(this));
|
|
if (hashes.length === 1) {
|
|
return hashes[0];
|
|
}
|
|
const hash = createHash('sha1');
|
|
hashes.forEach(h => hash.update(h));
|
|
return hash.digest('hex');
|
|
} catch (err) {
|
|
return console.log('fishing');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computes the hash of a given file.
|
|
* @param {string} file File path
|
|
*/
|
|
computeFileHash (file) {
|
|
try {
|
|
if (!existsSync(file)) {
|
|
throw new Error('File doesn\'t exist!');
|
|
}
|
|
const fileBuffer = readFileSync(file);
|
|
return createHash('sha1')
|
|
.update(this._metadata)
|
|
.update(fileBuffer)
|
|
.digest('hex');
|
|
} catch (err) {
|
|
return console.log('fishing');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compiles the file. Should NOT perform any cache-related actions.
|
|
* @returns {Promise<string>} Compilation results.
|
|
*/
|
|
_compile () {
|
|
throw new Error('Not implemented');
|
|
}
|
|
|
|
/**
|
|
* @returns {string} Compiler metadata (compiler used, version)
|
|
*/
|
|
get _metadata () {
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {...any} message
|
|
* @private
|
|
*/
|
|
_log (...message) {
|
|
return require('@vizality/util/logger').log({ labels: this._labels, message });
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {...any} message
|
|
* @private
|
|
*/
|
|
_warn (...message) {
|
|
return require('@vizality/util/logger').warn({ labels: this._labels, message });
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {...any} message
|
|
* @private
|
|
*/
|
|
_error (...message) {
|
|
return require('@vizality/util/logger').error({ labels: this._labels, message });
|
|
}
|
|
};
|