UNPKG

piral-debug-utils

Version:

Utilities for debugging Piral instances.

441 lines 16.2 kB
import { changeExtensionCatalogueStore, ExtensionCatalogue } from './ExtensionCatalogue'; import { decycle } from './decycle'; import { overlayId } from './overlay'; import { createVisualizer, destroyVisualizer, toggleVisualizer } from './visualizer'; import { getInitialSettings, initialSetter, enablePersistance, disablePersistance, settingsKeys } from './state'; export function installPiralDebug(options) { const { getGlobalState, getExtensions, getDependencies, getRoutes, getPilets, fireEvent, integrate, removePilet, updatePilet, addPilet, navigate, emulator = true, customSettings = {}, defaultSettings = {}, } = options; const events = []; const legacyBrowser = !new Error().stack; const selfSource = 'piral-debug-api'; const debugApiVersion = 'v1'; let setValue = initialSetter; const initialSettings = getInitialSettings(defaultSettings); const emulatorSettings = emulator ? { loadPilets: { value: initialSettings.loadPilets, type: 'boolean', label: 'Load available pilets', group: 'general', onChange(value) { setValue(settingsKeys.loadPilets, value ? 'on' : 'off'); }, }, hardRefresh: { value: initialSettings.hardRefresh, type: 'boolean', label: 'Full refresh on change', group: 'general', onChange(value) { setValue(settingsKeys.hardRefresh, value ? 'on' : 'off'); }, }, } : {}; const settings = { ...customSettings, viewState: { value: initialSettings.viewState, type: 'boolean', label: 'State container logging', group: 'general', onChange(value) { setValue(settingsKeys.viewState, value ? 'on' : 'off'); }, }, ...emulatorSettings, viewOrigins: { value: initialSettings.viewOrigins, type: 'boolean', label: 'Visualize component origins', group: 'extensions', onChange(value, prev) { setValue(settingsKeys.viewOrigins, value ? 'on' : 'off'); if (prev !== value) { updateVisualize(value); } }, }, errorOverlay: { value: initialSettings.errorOverlay, type: 'boolean', label: 'Show error overlay', group: 'extensions', onChange(value) { setValue(settingsKeys.errorOverlay, value ? 'on' : 'off'); }, }, extensionCatalogue: { value: initialSettings.extensionCatalogue, type: 'boolean', label: 'Enable extension catalogue', group: 'extensions', onChange(value) { setValue(settingsKeys.extensionCatalogue, value ? 'on' : 'off'); }, }, clearConsole: { value: initialSettings.clearConsole, type: 'boolean', label: 'Clear console during HMR', group: 'general', onChange(value) { setValue(settingsKeys.clearConsole, value ? 'on' : 'off'); }, }, persistSettings: { value: initialSettings.persistSettings, type: 'boolean', label: 'Persist settings', group: 'inspector', onChange(value) { setValue = value ? enablePersistance() : disablePersistance(); }, }, }; const excludedRoutes = [initialSettings.cataloguePath]; if (initialSettings.viewOrigins) { createVisualizer(); } const sendMessage = (content) => { window.postMessage({ content, source: selfSource, version: debugApiVersion, }, '*'); }; const getSettings = () => { return Object.keys(settings).reduce((obj, key) => { const setting = settings[key]; if (setting && typeof setting === 'object' && typeof setting.label === 'string' && typeof setting.type === 'string' && ['boolean', 'string', 'number'].includes(typeof setting.value)) { obj[key] = { label: setting.label, value: setting.value, type: setting.type, }; } return obj; }, {}); }; const updateSettings = (values) => { Object.keys(values).forEach((key) => { const setting = settings[key]; switch (setting.type) { case 'boolean': { const prev = setting.value; const value = values[key]; setting.value = value; setting.onChange(value, prev); break; } case 'number': { const prev = setting.value; const value = values[key]; setting.value = value; setting.onChange(value, prev); break; } case 'string': { const prev = setting.value; const value = values[key]; setting.value = value; setting.onChange(value, prev); break; } } }); sendMessage({ settings: getSettings(), type: 'settings', }); }; const togglePilet = (name) => { const pilet = getPilets().find((m) => m.name === name); if (!pilet) { // nothing to do, obviously invalid call } else if (pilet.disabled) { if (pilet.original) { // everything is fine, let's use the cached version updatePilet(pilet.original); } else { // something fishy is going on - let's just try to activate the same pilet updatePilet({ ...pilet, disabled: false }); } } else { updatePilet({ name, disabled: true, original: pilet }); } }; const updateVisualize = (active) => { if (active) { createVisualizer(); } else { destroyVisualizer(); } }; const eventDispatcher = document.body.dispatchEvent; const systemResolve = System.constructor.prototype.resolve; const depMap = {}; const subDeps = {}; const findAncestor = (parent) => { while (subDeps[parent]) { parent = subDeps[parent]; } return parent; }; System.constructor.prototype.resolve = function (...args) { const [url, parent] = args; const result = systemResolve.call(this, ...args); if (!parent) { return result; } const ancestor = findAncestor(parent); if (url.startsWith('./')) { subDeps[result] = ancestor; } else { const deps = depMap[ancestor] || {}; deps[url] = result; depMap[ancestor] = deps; } return result; }; const debugApi = { debug: debugApiVersion, instance: { name: process.env.BUILD_PCKG_NAME, version: process.env.BUILD_PCKG_VERSION, dependencies: process.env.SHARED_DEPENDENCIES, }, build: { date: process.env.BUILD_TIME_FULL, cli: process.env.PIRAL_CLI_VERSION, compat: process.env.DEBUG_PIRAL, }, }; const details = { name: debugApi.instance.name, version: debugApi.instance.version, kind: debugApiVersion, mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', capabilities: [ 'events', 'container', 'routes', 'pilets', 'settings', 'extensions', 'dependencies', 'dependency-map', ], }; const start = () => { const container = decycle(getGlobalState()); const routes = getRoutes().filter((r) => !excludedRoutes.includes(r)); const extensions = getExtensions(); const settings = getSettings(); const dependencies = getDependencies(); const pilets = getPilets().map((pilet) => ({ name: pilet.name, version: pilet.version, disabled: pilet.disabled, })); sendMessage({ type: 'available', ...details, state: { routes, pilets, container, settings, events, extensions, dependencies, }, }); }; const check = () => { sendMessage({ type: 'info', ...details, }); }; const getDependencyMap = () => { const dependencyMap = {}; const addDeps = (pilet, dependencies) => { const deps = dependencyMap[pilet] || []; for (const depName of Object.keys(dependencies)) { if (!deps.some((m) => m.demanded === depName)) { deps.push({ demanded: depName, resolved: dependencies[depName], }); } } dependencyMap[pilet] = deps; }; const pilets = getPilets() .map((pilet) => ({ name: pilet.name, link: pilet.link, base: pilet.base, })) .filter((m) => m.link); Object.keys(depMap).forEach((url) => { const dependencies = depMap[url]; const pilet = pilets.find((p) => p.link === url); if (pilet) { addDeps(pilet.name, dependencies); } else if (!pilet) { const parent = pilets.find((p) => url.startsWith(p.base)); if (parent) { addDeps(parent.name, dependencies); } } }); sendMessage({ type: 'dependency-map', dependencyMap, }); }; document.body.dispatchEvent = function (ev) { if (ev.type.startsWith('piral-')) { const name = ev.type.replace('piral-', ''); const args = ev.detail.arg; events.unshift({ id: events.length.toString(), name, args: decycle(args), time: Date.now(), }); if (name === 'unhandled-error' && args.errorType && typeof customElements !== 'undefined' && sessionStorage.getItem(settingsKeys.errorOverlay) !== 'off') { const ErrorOverlay = customElements.get(overlayId); document.body.appendChild(new ErrorOverlay(args)); } sendMessage({ events, type: 'events', }); } return eventDispatcher.call(this, ev); }; window.addEventListener('storage', (event) => { if (!legacyBrowser && event.storageArea === sessionStorage) { // potentially unknowingly updated settings updateSettings({ viewState: sessionStorage.getItem(settingsKeys.viewState) !== 'off', loadPilets: sessionStorage.getItem(settingsKeys.loadPilets) === 'on', hardRefresh: sessionStorage.getItem(settingsKeys.hardRefresh) === 'on', viewOrigins: sessionStorage.getItem(settingsKeys.viewOrigins) === 'on', extensionCatalogue: sessionStorage.getItem(settingsKeys.extensionCatalogue) !== 'off', clearConsole: sessionStorage.getItem(settingsKeys.clearConsole) === 'on', errorOverlay: sessionStorage.getItem(settingsKeys.errorOverlay) !== 'off', }); } }); window.addEventListener('message', (event) => { const { source, version, content } = event.data; if (source !== selfSource && version === debugApiVersion) { switch (content.type) { case 'init': return start(); case 'check-piral': return check(); case 'get-dependency-map': return getDependencyMap(); case 'update-settings': return updateSettings(content.settings); case 'append-pilet': return addPilet(content.meta); case 'remove-pilet': return removePilet(content.name); case 'toggle-pilet': return togglePilet(content.name); case 'emit-event': return fireEvent(content.name, content.args); case 'goto-route': if (content.route === initialSettings.cataloguePath && content.state) { changeExtensionCatalogueStore(content.state); } return navigate(content.route, content.state); case 'visualize-all': return toggleVisualizer(); } } }); integrate({ routes: { [initialSettings.cataloguePath]: ExtensionCatalogue, }, onChange(previous, current, changed) { if (changed.state) { if (settings.viewState.value) { if (!legacyBrowser) { // Chrome, Firefox, ... (full capability) const err = new Error(); const lastLine = err.stack.split('\n')[6]; if (lastLine) { const action = lastLine.replace(/^\s+at\s+(Atom\.|Object\.)?/, ''); console.group(`%c Piral State Change %c ${new Date().toLocaleTimeString()}`, 'color: gray; font-weight: lighter;', 'color: black; font-weight: bold;'); console.log('%c Previous', `color: #9E9E9E; font-weight: bold`, previous); console.log('%c Action', `color: #03A9F4; font-weight: bold`, action); console.log('%c Next', `color: #4CAF50; font-weight: bold`, current); console.groupEnd(); } } else { // IE 11, ... (does not know colors etc.) console.log('Changed state', previous, current); } } sendMessage({ type: 'container', container: decycle(getGlobalState()), }); } if (changed.pilets) { sendMessage({ type: 'pilets', pilets: getPilets().map((pilet) => ({ name: pilet.name, version: pilet.version, disabled: !!pilet.disabled, })), }); } if (changed.pages) { sendMessage({ type: 'routes', routes: getRoutes().filter((r) => !excludedRoutes.includes(r)), }); } if (changed.extensions) { sendMessage({ type: 'extensions', extensions: getExtensions(), }); } if (changed.dependencies) { sendMessage({ type: 'dependencies', dependencies: getDependencies(), }); } }, }); window['dbg:piral'] = debugApi; start(); } //# sourceMappingURL=debug.js.map