piral-debug-utils
Advanced tools
Comparing version 1.0.0-pre.2096 to 1.0.0
@@ -1,9 +0,2 @@ | ||
import { PiletApiCreator, PiletDependencyGetter, PiletLoader, PiletRequester } from 'piral-base'; | ||
export interface DebuggerOptions { | ||
createApi: PiletApiCreator; | ||
getDependencies: PiletDependencyGetter; | ||
loadPilet: PiletLoader; | ||
requestPilets: PiletRequester; | ||
context?: any; | ||
} | ||
import { DebuggerOptions } from './types'; | ||
export declare function installPiralDebug(options: DebuggerOptions): void; |
403
lib/debug.js
"use strict"; | ||
var __rest = (this && this.__rest) || function (s, e) { | ||
var t = {}; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) | ||
t[p] = s[p]; | ||
if (s != null && typeof Object.getOwnPropertySymbols === "function") | ||
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { | ||
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) | ||
t[p[i]] = s[p[i]]; | ||
} | ||
return t; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.installPiralDebug = void 0; | ||
const DebugTracker_1 = require("./DebugTracker"); | ||
const VisualizationWrapper_1 = require("./VisualizationWrapper"); | ||
const ExtensionCatalogue_1 = require("./ExtensionCatalogue"); | ||
const decycle_1 = require("./decycle"); | ||
const state_1 = require("./state"); | ||
function installPiralDebug(options) { | ||
const { context } = options, pilets = __rest(options, ["context"]); | ||
// the DEBUG_PIRAL env should contain the Piral CLI compatibility version | ||
window['dbg:piral'] = { | ||
debug: 'v0', | ||
const { getGlobalState, getExtensions, getDependencies, getRoutes, getPilets, fireEvent, integrate, removePilet, updatePilet, addPilet, navigate, customSettings = {}, defaultSettings = {}, } = options; | ||
const events = []; | ||
const legacyBrowser = !new Error().stack; | ||
const initialSettings = (0, state_1.getInitialSettings)(defaultSettings); | ||
const excludedRoutes = [initialSettings.cataloguePath]; | ||
const selfSource = 'piral-debug-api'; | ||
const debugApiVersion = 'v1'; | ||
let setValue = state_1.initialSetter; | ||
(0, state_1.setInitialState)(initialSettings); | ||
const settings = Object.assign(Object.assign({}, customSettings), { viewState: { | ||
value: initialSettings.viewState, | ||
type: 'boolean', | ||
label: 'State container logging', | ||
onChange(value) { | ||
setValue(state_1.settingsKeys.viewState, value ? 'on' : 'off'); | ||
}, | ||
}, loadPilets: { | ||
value: initialSettings.loadPilets, | ||
type: 'boolean', | ||
label: 'Load available pilets', | ||
onChange(value) { | ||
setValue(state_1.settingsKeys.loadPilets, value ? 'on' : 'off'); | ||
}, | ||
}, hardRefresh: { | ||
value: initialSettings.hardRefresh, | ||
type: 'boolean', | ||
label: 'Full refresh on change', | ||
onChange(value) { | ||
setValue(state_1.settingsKeys.hardRefresh, value ? 'on' : 'off'); | ||
}, | ||
}, viewOrigins: { | ||
value: initialSettings.viewOrigins, | ||
type: 'boolean', | ||
label: 'Visualize component origins', | ||
onChange(value, prev) { | ||
setValue(state_1.settingsKeys.viewOrigins, value ? 'on' : 'off'); | ||
if (prev !== value) { | ||
updateVisualize(value); | ||
} | ||
}, | ||
}, extensionCatalogue: { | ||
value: initialSettings.extensionCatalogue, | ||
type: 'boolean', | ||
label: 'Enable extension catalogue', | ||
onChange(value) { | ||
setValue(state_1.settingsKeys.extensionCatalogue, value ? 'on' : 'off'); | ||
}, | ||
}, clearConsole: { | ||
value: initialSettings.clearConsole, | ||
type: 'boolean', | ||
label: 'Clear console during HMR', | ||
onChange(value) { | ||
setValue(state_1.settingsKeys.clearConsole, value ? 'on' : 'off'); | ||
}, | ||
}, persistSettings: { | ||
value: initialSettings.persistSettings, | ||
type: 'boolean', | ||
label: 'Persist settings', | ||
onChange(value) { | ||
setValue = value ? (0, state_1.enablePersistance)() : (0, state_1.disablePersistance)(); | ||
}, | ||
} }); | ||
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(Object.assign(Object.assign({}, pilet), { disabled: false })); | ||
} | ||
} | ||
else { | ||
updatePilet({ name, disabled: true, original: pilet }); | ||
} | ||
}; | ||
const toggleVisualize = () => { | ||
(0, state_1.setState)((s) => (Object.assign(Object.assign({}, s), { visualize: Object.assign(Object.assign({}, s.visualize), { force: !s.visualize.force }) }))); | ||
}; | ||
const updateVisualize = (active) => { | ||
(0, state_1.setState)((s) => (Object.assign(Object.assign({}, s), { visualize: Object.assign(Object.assign({}, s.visualize), { active }) }))); | ||
}; | ||
const goToRoute = (path, state) => { | ||
(0, state_1.setState)((s) => (Object.assign(Object.assign({}, s), { route: { | ||
path, | ||
state, | ||
} }))); | ||
}; | ||
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: { | ||
@@ -24,3 +192,2 @@ name: process.env.BUILD_PCKG_NAME, | ||
dependencies: process.env.SHARED_DEPENDENCIES, | ||
context, | ||
}, | ||
@@ -32,6 +199,208 @@ build: { | ||
}, | ||
pilets, | ||
}; | ||
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 = (0, decycle_1.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(Object.assign(Object.assign({ type: 'available' }, details), { state: { | ||
routes, | ||
pilets, | ||
container, | ||
settings, | ||
events, | ||
extensions, | ||
dependencies, | ||
} })); | ||
}; | ||
const check = () => { | ||
sendMessage(Object.assign({ 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-')) { | ||
events.unshift({ | ||
id: events.length.toString(), | ||
name: ev.type.replace('piral-', ''), | ||
args: (0, decycle_1.decycle)(ev.detail.arg), | ||
time: Date.now(), | ||
}); | ||
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(state_1.settingsKeys.viewState) !== 'off', | ||
loadPilets: sessionStorage.getItem(state_1.settingsKeys.loadPilets) === 'on', | ||
hardRefresh: sessionStorage.getItem(state_1.settingsKeys.hardRefresh) === 'on', | ||
viewOrigins: sessionStorage.getItem(state_1.settingsKeys.viewOrigins) === 'on', | ||
extensionCatalogue: sessionStorage.getItem(state_1.settingsKeys.extensionCatalogue) !== 'off', | ||
clearConsole: sessionStorage.getItem(state_1.settingsKeys.clearConsole) === 'on', | ||
}); | ||
} | ||
}); | ||
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': | ||
return goToRoute(content.route, content.state); | ||
case 'visualize-all': | ||
return toggleVisualize(); | ||
} | ||
} | ||
}); | ||
(0, state_1.setNavigate)(navigate); | ||
integrate({ | ||
components: { | ||
Debug: DebugTracker_1.DebugTracker, | ||
}, | ||
routes: { | ||
[initialSettings.cataloguePath]: ExtensionCatalogue_1.ExtensionCatalogue, | ||
}, | ||
wrappers: { | ||
'*': VisualizationWrapper_1.VisualizationWrapper, | ||
}, | ||
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: (0, decycle_1.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(); | ||
} | ||
exports.installPiralDebug = installPiralDebug; | ||
//# sourceMappingURL=debug.js.map |
@@ -1,8 +0,3 @@ | ||
import { Pilet, PiletApiCreator, PiletLoader, PiletRequester } from 'piral-base'; | ||
export interface EmulatorConnectorOptions { | ||
createApi: PiletApiCreator; | ||
loadPilet: PiletLoader; | ||
inject?(pilet: Pilet): void; | ||
piletApiFallback?: string; | ||
} | ||
export declare function withEmulatorPilets(requestPilets: PiletRequester, options: EmulatorConnectorOptions): PiletRequester; | ||
import type { PiletRequester } from 'piral-base'; | ||
import type { EmulatorConnectorOptions } from './types'; | ||
export declare function installPiletEmulator(requestPilets: PiletRequester, options: EmulatorConnectorOptions): void; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.withEmulatorPilets = void 0; | ||
const piral_base_1 = require("piral-base"); | ||
function withEmulatorPilets(requestPilets, options) { | ||
const { loadPilet, createApi, inject, piletApiFallback = '/$pilet-api' } = options; | ||
// check if pilets should be loaded | ||
const loadPilets = sessionStorage.getItem('dbg:load-pilets') === 'on'; | ||
const noPilets = () => Promise.resolve([]); | ||
const requester = loadPilets ? requestPilets : noPilets; | ||
return () => { | ||
exports.installPiletEmulator = void 0; | ||
const routeRefresh_1 = require("./routeRefresh"); | ||
function installPiletEmulator(requestPilets, options) { | ||
const { addPilet, removePilet, integrate, piletApiFallback = '/$pilet-api' } = options; | ||
integrate(() => { | ||
// check if pilets should be loaded | ||
const loadPilets = sessionStorage.getItem('dbg:load-pilets') === 'on'; | ||
const noPilets = () => Promise.resolve([]); | ||
const requester = loadPilets ? requestPilets : noPilets; | ||
const promise = requester(); | ||
@@ -21,2 +21,4 @@ // the window['dbg:pilet-api'] should point to an API address used as a proxy, fall back to '/$pilet-api' if unavailable | ||
const ws = new WebSocket(updateTarget); | ||
const timeoutCache = {}; | ||
const timeout = 150; | ||
const appendix = fetch(initialTarget) | ||
@@ -30,13 +32,24 @@ .then((res) => res.json()) | ||
const meta = JSON.parse(data); | ||
loadPilet(meta).then((pilet) => { | ||
try { | ||
if (piral_base_1.isfunc(inject)) { | ||
inject(pilet); | ||
const name = meta.name; | ||
// like a debounce; only one change of the current pilet should be actively processed | ||
clearTimeout(timeoutCache[name]); | ||
// some bundlers may have fired before writing to the disk | ||
// so we give them a bit of time before actually loading the pilet | ||
timeoutCache[name] = setTimeout(() => { | ||
// we should make sure to only refresh the page / router if pilets have been loaded | ||
const unfreeze = (0, routeRefresh_1.freezeRouteRefresh)(); | ||
// tear down pilet | ||
removePilet(meta.name) | ||
.then(() => { | ||
const clearConsole = sessionStorage.getItem('dbg:clear-console') === 'on'; | ||
if (clearConsole) { | ||
console.clear(); | ||
} | ||
piral_base_1.setupPilet(pilet, createApi); | ||
} | ||
catch (error) { | ||
console.error(error); | ||
} | ||
}); | ||
console.log('Updating pilet %c%s ...', 'color: green; background: white; font-weight: bold', name); | ||
}) | ||
// load and evaluate pilet | ||
.then(() => addPilet(meta)) | ||
// then disable route cache, should be zero again and lead to route refresh | ||
.then(unfreeze, unfreeze); | ||
}, timeout); | ||
} | ||
@@ -52,6 +65,10 @@ else { | ||
}) | ||
.then((pilets) => appendix.then((debugPilets) => [...pilets, ...debugPilets])); | ||
}; | ||
.then((pilets) => appendix.then((debugPilets) => { | ||
const debugPiletNames = debugPilets.map((m) => m.name); | ||
const feedPilets = pilets.filter((m) => !debugPiletNames.includes(m.name)); | ||
return [...feedPilets, ...debugPilets]; | ||
})); | ||
}); | ||
} | ||
exports.withEmulatorPilets = withEmulatorPilets; | ||
exports.installPiletEmulator = installPiletEmulator; | ||
//# sourceMappingURL=emulator.js.map |
export * from './debug'; | ||
export * from './emulator'; | ||
export * from './types'; | ||
export * from './useDebugRouteFilter'; | ||
export * from './web'; |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
__exportStar(require("./debug"), exports); | ||
__exportStar(require("./emulator"), exports); | ||
const tslib_1 = require("tslib"); | ||
tslib_1.__exportStar(require("./debug"), exports); | ||
tslib_1.__exportStar(require("./emulator"), exports); | ||
tslib_1.__exportStar(require("./types"), exports); | ||
tslib_1.__exportStar(require("./useDebugRouteFilter"), exports); | ||
tslib_1.__exportStar(require("./web"), exports); | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "piral-debug-utils", | ||
"version": "1.0.0-pre.2096", | ||
"version": "1.0.0", | ||
"description": "Utilities for debugging Piral instances.", | ||
@@ -17,7 +17,39 @@ "keywords": [ | ||
"license": "MIT", | ||
"module": "esm/index.js", | ||
"main": "lib/index.js", | ||
"typings": "lib/index.d.ts", | ||
"exports": { | ||
".": { | ||
"import": "./esm/index.js", | ||
"require": "./lib/index.js" | ||
}, | ||
"./esm/*": { | ||
"import": "./esm/*" | ||
}, | ||
"./lib/*": { | ||
"require": "./lib/*" | ||
}, | ||
"./debug": { | ||
"import": "./esm/debug.js", | ||
"require": "./lib/debug.js", | ||
"types": "./lib/debug.d.ts" | ||
}, | ||
"./emulator": { | ||
"import": "./esm/emulator.js", | ||
"require": "./lib/emulator.js", | ||
"types": "./lib/emulator.d.ts" | ||
}, | ||
"./_/*": { | ||
"import": "./esm/*.js", | ||
"require": "./lib/*.js" | ||
}, | ||
"./package.json": "./package.json" | ||
}, | ||
"sideEffects": false, | ||
"files": [ | ||
"esm", | ||
"lib", | ||
"src" | ||
"src", | ||
"debug.d.ts", | ||
"emulator.d.ts" | ||
], | ||
@@ -32,3 +64,6 @@ "repository": { | ||
"scripts": { | ||
"build": "tsc", | ||
"cleanup": "rimraf esm lib", | ||
"build": "yarn build:commonjs && yarn build:esnext", | ||
"build:commonjs": "tsc --project tsconfig.json --outDir lib --module commonjs", | ||
"build:esnext": "tsc --project tsconfig.json --outDir esm --module esnext", | ||
"typedoc": "typedoc --json ../../../docs/types/piral-debug-utils.json src --exclude \"src/**/*.test.*\"", | ||
@@ -38,8 +73,9 @@ "test": "echo \"Error: run tests from root\" && exit 1" | ||
"devDependencies": { | ||
"piral-base": "^1.0.0-pre.2096" | ||
"piral-base": "^1.0.0" | ||
}, | ||
"peerDependencies": { | ||
"piral-base": "0.12.x || 1.x" | ||
"react": ">=16.8.0", | ||
"react-router": ">=5.0.0" | ||
}, | ||
"gitHead": "9ac3626cc85b5937a50e039debd32c2697b49242" | ||
"gitHead": "67d9a2920bd5231baf10bc87ae8985666b18fa3a" | ||
} |
@@ -1,4 +0,4 @@ | ||
[![Piral Logo](https://github.com/smapiot/piral/raw/master/docs/assets/logo.png)](https://piral.io) | ||
[![Piral Logo](https://github.com/smapiot/piral/raw/main/docs/assets/logo.png)](https://piral.io) | ||
# [Piral Debug Utils](https://piral.io) · [![GitHub License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/smapiot/piral/blob/master/LICENSE) [![npm version](https://img.shields.io/npm/v/piral-debug-utils.svg?style=flat)](https://www.npmjs.com/package/piral-debug-utils) [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://jestjs.io) [![Gitter Chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/piral-io/community) | ||
# [Piral Debug Utils](https://piral.io) · [![GitHub License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/smapiot/piral/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/piral-debug-utils.svg?style=flat)](https://www.npmjs.com/package/piral-debug-utils) [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://jestjs.io) [![Gitter Chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/piral-io/community) | ||
@@ -11,3 +11,3 @@ This is a utility library that can be used for debugging Piral instances. | ||
This should only be installed as a dependency (`dependencies`), but usually guarded to be active (or included in the bundle) only for development / emulation purposes (i.e., when developing pilets). | ||
This should only be installed as a dependency (`dependencies`), but usually guarded to be active (or included in the bundle) only for development/emulation purposes (i.e., when developing pilets). | ||
@@ -36,11 +36,17 @@ If you'd love to use yarn: | ||
// if we build the debug version of piral (debug and emulator build) | ||
if (process.env.DEBUG_PIRAL !== undefined) { | ||
if (process.env.DEBUG_PIRAL) { | ||
const { installPiralDebug } = require('piral-debug-utils'); | ||
installPiralDebug({ | ||
context, | ||
createApi, | ||
getDependencies, | ||
loadPilet, | ||
requestPilets, | ||
fireEvent, | ||
getGlobalState, | ||
getPilets, | ||
getExtensions, | ||
getRoutes, | ||
integrate, | ||
addPilet, | ||
removePilet, | ||
updatePilet, | ||
navigate, | ||
}); | ||
@@ -50,3 +56,3 @@ } | ||
We can use the `withEmulatorPilets` function to modify (or not) the provided `PiletRequester`, which will be handed over later to the `createInstance` options or `piral-base` directly. | ||
We can use the `installPiletEmulator` function to modify (or not) the provided `PiletRequester`, which will be handed over later to the `createInstance` options or `piral-base` directly. | ||
@@ -56,10 +62,10 @@ Usually, we'd guard it to make it only accessible under emulator conditions. | ||
```js | ||
// if we want to change `fetchPilets` (for an emulator) of the LoadPiletsOptions | ||
if (process.env.DEBUG_PILET !== undefined) { | ||
const { withEmulatorPilets } = require('piral-debug-utils'); | ||
// if we want to change `requestPilets` (for an emulator) of the LoadPiletsOptions | ||
if (process.env.DEBUG_PILET) { | ||
const { installPiletEmulator } = require('piral-debug-utils'); | ||
fetchPilets = withEmulatorPilets(fetchPilets, { | ||
inject: context.injectPilet, | ||
createApi, | ||
loadPilet, | ||
installPiletEmulator(requestPilets, { | ||
addPilet, | ||
removePilet, | ||
integrate, | ||
}); | ||
@@ -66,0 +72,0 @@ } |
509
src/debug.ts
@@ -1,17 +0,260 @@ | ||
import { PiletApiCreator, PiletDependencyGetter, PiletLoader, PiletRequester } from 'piral-base'; | ||
import { DebugTracker } from './DebugTracker'; | ||
import { VisualizationWrapper } from './VisualizationWrapper'; | ||
import { ExtensionCatalogue } from './ExtensionCatalogue'; | ||
import { decycle } from './decycle'; | ||
import { DebugCustomSetting, DebuggerOptions } from './types'; | ||
import { | ||
setState, | ||
getInitialSettings, | ||
setInitialState, | ||
setNavigate, | ||
initialSetter, | ||
enablePersistance, | ||
disablePersistance, | ||
settingsKeys, | ||
} from './state'; | ||
export interface DebuggerOptions { | ||
createApi: PiletApiCreator; | ||
getDependencies: PiletDependencyGetter; | ||
loadPilet: PiletLoader; | ||
requestPilets: PiletRequester; | ||
context?: any; | ||
} | ||
export function installPiralDebug(options: DebuggerOptions) { | ||
const { context, ...pilets } = options; | ||
const { | ||
getGlobalState, | ||
getExtensions, | ||
getDependencies, | ||
getRoutes, | ||
getPilets, | ||
fireEvent, | ||
integrate, | ||
removePilet, | ||
updatePilet, | ||
addPilet, | ||
navigate, | ||
customSettings = {}, | ||
defaultSettings = {}, | ||
} = options; | ||
const events = []; | ||
const legacyBrowser = !new Error().stack; | ||
const initialSettings = getInitialSettings(defaultSettings); | ||
const excludedRoutes = [initialSettings.cataloguePath]; | ||
const selfSource = 'piral-debug-api'; | ||
const debugApiVersion = 'v1'; | ||
let setValue = initialSetter; | ||
// the DEBUG_PIRAL env should contain the Piral CLI compatibility version | ||
window['dbg:piral'] = { | ||
debug: 'v0', | ||
setInitialState(initialSettings); | ||
const settings: Record<string, DebugCustomSetting> = { | ||
...customSettings, | ||
viewState: { | ||
value: initialSettings.viewState, | ||
type: 'boolean', | ||
label: 'State container logging', | ||
onChange(value) { | ||
setValue(settingsKeys.viewState, value ? 'on' : 'off'); | ||
}, | ||
}, | ||
loadPilets: { | ||
value: initialSettings.loadPilets, | ||
type: 'boolean', | ||
label: 'Load available pilets', | ||
onChange(value) { | ||
setValue(settingsKeys.loadPilets, value ? 'on' : 'off'); | ||
}, | ||
}, | ||
hardRefresh: { | ||
value: initialSettings.hardRefresh, | ||
type: 'boolean', | ||
label: 'Full refresh on change', | ||
onChange(value) { | ||
setValue(settingsKeys.hardRefresh, value ? 'on' : 'off'); | ||
}, | ||
}, | ||
viewOrigins: { | ||
value: initialSettings.viewOrigins, | ||
type: 'boolean', | ||
label: 'Visualize component origins', | ||
onChange(value, prev) { | ||
setValue(settingsKeys.viewOrigins, value ? 'on' : 'off'); | ||
if (prev !== value) { | ||
updateVisualize(value); | ||
} | ||
}, | ||
}, | ||
extensionCatalogue: { | ||
value: initialSettings.extensionCatalogue, | ||
type: 'boolean', | ||
label: 'Enable extension catalogue', | ||
onChange(value) { | ||
setValue(settingsKeys.extensionCatalogue, value ? 'on' : 'off'); | ||
}, | ||
}, | ||
clearConsole: { | ||
value: initialSettings.clearConsole, | ||
type: 'boolean', | ||
label: 'Clear console during HMR', | ||
onChange(value) { | ||
setValue(settingsKeys.clearConsole, value ? 'on' : 'off'); | ||
}, | ||
}, | ||
persistSettings: { | ||
value: initialSettings.persistSettings, | ||
type: 'boolean', | ||
label: 'Persist settings', | ||
onChange(value) { | ||
setValue = value ? enablePersistance() : disablePersistance(); | ||
}, | ||
}, | ||
}; | ||
const sendMessage = (content: any) => { | ||
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: Record<string, any>) => { | ||
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: string) => { | ||
const pilet: any = 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 toggleVisualize = () => { | ||
setState((s) => ({ | ||
...s, | ||
visualize: { | ||
...s.visualize, | ||
force: !s.visualize.force, | ||
}, | ||
})); | ||
}; | ||
const updateVisualize = (active: boolean) => { | ||
setState((s) => ({ | ||
...s, | ||
visualize: { | ||
...s.visualize, | ||
active, | ||
}, | ||
})); | ||
}; | ||
const goToRoute = (path: string, state?: any) => { | ||
setState((s) => ({ | ||
...s, | ||
route: { | ||
path, | ||
state, | ||
}, | ||
})); | ||
}; | ||
const eventDispatcher = document.body.dispatchEvent; | ||
const systemResolve = System.constructor.prototype.resolve; | ||
const depMap: Record<string, Record<string, string>> = {}; | ||
const subDeps: Record<string, string> = {}; | ||
const findAncestor = (parent: string) => { | ||
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: { | ||
@@ -21,3 +264,2 @@ name: process.env.BUILD_PCKG_NAME, | ||
dependencies: process.env.SHARED_DEPENDENCIES, | ||
context, | ||
}, | ||
@@ -29,4 +271,241 @@ build: { | ||
}, | ||
pilets, | ||
}; | ||
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: any) => ({ | ||
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: Record<string, Array<{ demanded: string; resolved: string }>> = {}; | ||
const addDeps = (pilet: string, dependencies: Record<string, string>) => { | ||
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: any) => ({ | ||
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: CustomEvent) { | ||
if (ev.type.startsWith('piral-')) { | ||
events.unshift({ | ||
id: events.length.toString(), | ||
name: ev.type.replace('piral-', ''), | ||
args: decycle(ev.detail.arg), | ||
time: Date.now(), | ||
}); | ||
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', | ||
}); | ||
} | ||
}); | ||
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': | ||
return goToRoute(content.route, content.state); | ||
case 'visualize-all': | ||
return toggleVisualize(); | ||
} | ||
} | ||
}); | ||
setNavigate(navigate); | ||
integrate({ | ||
components: { | ||
Debug: DebugTracker, | ||
}, | ||
routes: { | ||
[initialSettings.cataloguePath]: ExtensionCatalogue, | ||
}, | ||
wrappers: { | ||
'*': VisualizationWrapper, | ||
}, | ||
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: any) => ({ | ||
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(); | ||
} |
@@ -1,18 +0,13 @@ | ||
import { isfunc, Pilet, PiletApiCreator, PiletLoader, PiletRequester, setupPilet } from 'piral-base'; | ||
import { freezeRouteRefresh } from './routeRefresh'; | ||
import type { PiletRequester } from 'piral-base'; | ||
import type { EmulatorConnectorOptions } from './types'; | ||
export interface EmulatorConnectorOptions { | ||
createApi: PiletApiCreator; | ||
loadPilet: PiletLoader; | ||
inject?(pilet: Pilet): void; | ||
piletApiFallback?: string; | ||
} | ||
export function installPiletEmulator(requestPilets: PiletRequester, options: EmulatorConnectorOptions) { | ||
const { addPilet, removePilet, integrate, piletApiFallback = '/$pilet-api' } = options; | ||
export function withEmulatorPilets(requestPilets: PiletRequester, options: EmulatorConnectorOptions): PiletRequester { | ||
const { loadPilet, createApi, inject, piletApiFallback = '/$pilet-api' } = options; | ||
// check if pilets should be loaded | ||
const loadPilets = sessionStorage.getItem('dbg:load-pilets') === 'on'; | ||
const noPilets: PiletRequester = () => Promise.resolve([]); | ||
const requester = loadPilets ? requestPilets : noPilets; | ||
return () => { | ||
integrate(() => { | ||
// check if pilets should be loaded | ||
const loadPilets = sessionStorage.getItem('dbg:load-pilets') === 'on'; | ||
const noPilets: PiletRequester = () => Promise.resolve([]); | ||
const requester = loadPilets ? requestPilets : noPilets; | ||
const promise = requester(); | ||
@@ -29,2 +24,4 @@ | ||
const ws = new WebSocket(updateTarget); | ||
const timeoutCache = {}; | ||
const timeout = 150; | ||
@@ -41,13 +38,29 @@ const appendix = fetch(initialTarget) | ||
const meta = JSON.parse(data); | ||
loadPilet(meta).then((pilet) => { | ||
try { | ||
if (isfunc(inject)) { | ||
inject(pilet); | ||
} | ||
const name = meta.name; | ||
setupPilet(pilet, createApi); | ||
} catch (error) { | ||
console.error(error); | ||
} | ||
}); | ||
// like a debounce; only one change of the current pilet should be actively processed | ||
clearTimeout(timeoutCache[name]); | ||
// some bundlers may have fired before writing to the disk | ||
// so we give them a bit of time before actually loading the pilet | ||
timeoutCache[name] = setTimeout(() => { | ||
// we should make sure to only refresh the page / router if pilets have been loaded | ||
const unfreeze = freezeRouteRefresh(); | ||
// tear down pilet | ||
removePilet(meta.name) | ||
.then(() => { | ||
const clearConsole = sessionStorage.getItem('dbg:clear-console') === 'on'; | ||
if (clearConsole) { | ||
console.clear(); | ||
} | ||
console.log('Updating pilet %c%s ...', 'color: green; background: white; font-weight: bold', name); | ||
}) | ||
// load and evaluate pilet | ||
.then(() => addPilet(meta)) | ||
// then disable route cache, should be zero again and lead to route refresh | ||
.then(unfreeze, unfreeze); | ||
}, timeout); | ||
} else { | ||
@@ -63,4 +76,10 @@ location.reload(); | ||
}) | ||
.then((pilets) => appendix.then((debugPilets) => [...pilets, ...debugPilets])); | ||
}; | ||
.then((pilets) => | ||
appendix.then((debugPilets) => { | ||
const debugPiletNames = debugPilets.map((m) => m.name); | ||
const feedPilets = pilets.filter((m) => !debugPiletNames.includes(m.name)); | ||
return [...feedPilets, ...debugPilets]; | ||
}), | ||
); | ||
}); | ||
} |
export * from './debug'; | ||
export * from './emulator'; | ||
export * from './types'; | ||
export * from './useDebugRouteFilter'; | ||
export * from './web'; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 7 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
170808
89
3085
0
74
2
3847
14
3
10
129