UNPKG

reboost

Version:

A super fast dev server for rapid web development

243 lines (242 loc) 9.92 kB
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); } }