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.
vizality/renderer/src/compilers/Compiler.js

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 });
}
};