reboost
Version:
A super fast dev server for rapid web development
243 lines (242 loc) • 9.92 kB
JavaScript
const debug = (...data) => debugMode && console.log(...data);
const Reboost = {
// eslint-disable-next-line no-constant-condition
reload: () => (false && debugMode) ? console.log('TRIGGER RELOAD') : self.location.reload()
};
const Private = (() => {
const dependentsMap = new Map();
const setDependencies = (file, dependencies) => {
dependencies.forEach((dependency) => {
const dependents = dependentsMap.get(dependency);
if (!dependents) {
dependentsMap.set(dependency, new Set([file]));
}
else {
dependents.add(file);
}
});
};
const dependentTreeFor = (file) => {
const dependentTrees = [];
const dependents = dependentsMap.get(file) || [];
dependents.forEach((dFile) => {
dependentTrees.push(dependentTreeFor(dFile));
});
return {
file: file,
dependents: dependentTrees
};
};
return {
Hot_Map: new Map(),
Hot_Data_Map: new Map(),
setDependencies,
dependentTreeFor
};
})();
Object.defineProperty(Reboost, '[[Private]]', { get: () => Private });
{
const aSelf = self;
if (!aSelf.process) {
aSelf.process = { env: { NODE_ENV: mode } };
}
else {
let a = aSelf.process;
if (a)
a = a.env;
if (a)
a.NODE_ENV = mode;
}
aSelf['Reboost'] = Reboost;
}
{
const makeLoopGuard = (max) => {
let count = 0;
return {
call() {
if (++count > max) {
throw new Error(`Loop crossed the limit of ${max}`);
}
}
};
};
let lostConnection = false;
const connectWebsocket = () => {
const socket = new WebSocket(`ws://${address.replace(/^https?:\/\//, '')}`);
const fileLastChangedRecord = {};
let importer;
let loadImporter;
socket.addEventListener('open', () => {
console.log('[reboost] Connected to the server');
lostConnection = false;
loadImporter = new Promise((resolve) => {
import(`${address}/importer`).then((mod) => {
importer = mod.default;
resolve();
});
});
});
socket.addEventListener('message', async ({ data }) => {
const { type, file: emitterFile } = JSON.parse(data);
const { Hot_Map, Hot_Data_Map } = Private;
if (type === 'change') {
console.log(`[reboost] Changed ${emitterFile}`);
if (!hotReload) {
console.log('[reboost] Hot Reload is disabled. Triggering full reload.');
return Reboost.reload();
}
const fileLastUpdated = fileLastChangedRecord[emitterFile];
const now = fileLastChangedRecord[emitterFile] = Date.now();
// Apply Hot Reload only if file's last updated time is greater that 0.8s
if ((typeof fileLastUpdated === 'undefined') || (((now - fileLastUpdated) / 1000) > 0.8)) {
await loadImporter;
const guard = makeLoopGuard(1000);
const checkedFiles = new Set();
let bubbleUpDependents;
let nextBubbleUpDependents = [Private.dependentTreeFor(emitterFile)];
let emitterFileData;
let handler;
let updatedModuleInstance;
let hotData;
while (nextBubbleUpDependents.length > 0) {
guard.call();
bubbleUpDependents = nextBubbleUpDependents;
nextBubbleUpDependents = [];
for (const { file, dependents } of bubbleUpDependents) {
debug('[Hot Reload] Checking -', file);
if (checkedFiles.has(file))
continue;
checkedFiles.add(file);
if ((emitterFileData = Hot_Map.get(file))) {
if (emitterFileData.declined)
Reboost.reload();
hotData = {};
emitterFileData.listeners.forEach(({ dispose }) => dispose && dispose(hotData));
Hot_Data_Map.set(file, hotData);
updatedModuleInstance = await import(`${address}/transformed?q=${encodeURIComponent(file)}&t=${now}`);
updatedModuleInstance = importer.All(updatedModuleInstance);
// If the module is self accepted, just call the self accept handler
// and finish the update (don't bubble up)
if ((handler = emitterFileData.listeners.get(file)) && handler.accept) {
debug('[Hot Reload] Self accepted by', file);
handler.accept(updatedModuleInstance);
}
else {
dependents.forEach((tree) => {
if ((handler = emitterFileData.listeners.get(tree.file)) && handler.accept) {
debug('[Hot Reload] Accepted by', tree.file);
handler.accept(updatedModuleInstance);
}
else {
nextBubbleUpDependents.push(tree);
}
});
}
Hot_Data_Map.delete(file);
}
else if (dependents.length > 0) {
nextBubbleUpDependents.push(...dependents);
}
else {
debug('[Hot Reload] Triggering full page reload. The file has no parent -', file);
Reboost.reload();
}
}
if (nextBubbleUpDependents.length === 0) {
debug('[Hot Reload] Completed update');
}
}
}
}
else if (type === 'unlink') {
Reboost.reload();
}
});
socket.addEventListener('close', () => {
if (!lostConnection) {
lostConnection = true;
console.log('[reboost] Lost connection to the server. Trying to reconnect...');
}
setTimeout(() => connectWebsocket(), 5000);
});
};
connectWebsocket();
}
const getEmitterFileData = (emitterFile) => {
if (!Private.Hot_Map.has(emitterFile)) {
Private.Hot_Map.set(emitterFile, {
declined: false,
listeners: new Map()
});
}
return Private.Hot_Map.get(emitterFile);
};
const getListenerFileData = (emitterFile, listenerFile) => {
const listenedFileData = getEmitterFileData(emitterFile);
if (!listenedFileData.listeners.has(listenerFile))
listenedFileData.listeners.set(listenerFile, {});
return listenedFileData.listeners.get(listenerFile);
};
export class Hot {
constructor(filePath) {
this.filePath = filePath;
}
async callSetter(type, a, b) {
let dependencyFilePath;
let callback;
if (typeof a === 'function') {
// Self
dependencyFilePath = this.filePath;
callback = a;
}
else {
dependencyFilePath = await this.resolveDependency(a, type);
callback = b;
}
const listenerData = getListenerFileData(dependencyFilePath, this.filePath);
if (!listenerData[type])
listenerData[type] = callback;
}
async resolveDependency(dependency, fnName) {
const response = await fetch(`${address}/resolve?from=${this.filePath}&to=${dependency}`);
if (!response.ok) {
console.error(`[reboost] Unable to resolve dependency "${dependency}" of "${this.filePath}" while using hot.${fnName}()`);
return 'UNRESOLVED';
}
return response.text();
}
/** The data passed from the disposal callbacks */
get data() {
return Private.Hot_Data_Map.get(this.filePath);
}
/** The id of the module, it can be used as a key to store data about the module */
get id() {
return this.filePath;
}
accept(a, b) {
this.callSetter('accept', a, b);
}
dispose(a, b) {
this.callSetter('dispose', a, b);
}
async decline(dependency) {
getEmitterFileData(dependency || await this.resolveDependency(dependency, 'decline')).declined = true;
}
/** Invalidates the Hot Reload phase and causes a full page reload */
invalidate() {
Reboost.reload();
}
// TODO: Remove these in v1.0
get self() {
return {
accept: this.selfAccept.bind(this),
dispose: this.selfDispose.bind(this)
};
}
selfAccept(callback) {
this.accept(callback);
}
selfDispose(callback) {
this.dispose(callback);
}
}