Something went wrong. Try again.
experiments in a post-browser web
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372/** * Peek Development Utilities * * Hot-reload, debugging, and development helpers. * Only import in development - tree-shake in production. * * Usage: * import { enableHotReload, devTools } from 'peek://app/components/dev.js'; * * enableHotReload(); // Connect to dev server * devTools.inspect('peek-button'); // Inspect component */
import { registry, getComponent, loadState } from './registry.js';import { getTheme, getThemeTokens, getThemeNames } from './theme.js';import { getExtensionIds, getExtension } from './extension.js';import { version } from './version.js';
// Dev mode statelet isDevMode = false;let hotReloadSocket = null;const componentInstances = new WeakMap();
/** * Enable development mode */export function enableDevMode() { isDevMode = true;
// Add global debug object if (typeof window !== 'undefined') { window.__PEEK_DEV__ = { registry, theme: { current: getTheme, tokens: getThemeTokens, names: getThemeNames }, extensions: { ids: getExtensionIds, get: getExtension }, version, inspect: inspectComponent, listComponents: listComponents, stats: getStats };
console.log( '%c🔍 Peek Dev Mode Enabled', 'background: #007aff; color: white; padding: 4px 8px; border-radius: 4px;' ); console.log('Access dev tools via window.__PEEK_DEV__'); }}
/** * Check if dev mode is enabled */export function isDevModeEnabled() { return isDevMode;}
/** * Enable hot-reload connection * @param {Object} options - Hot reload options * @param {string} options.url - WebSocket server URL * @param {number} options.reconnectDelay - Reconnection delay in ms */export function enableHotReload(options = {}) { const { url = 'ws://localhost:35729/livereload', reconnectDelay = 1000 } = options;
if (typeof WebSocket === 'undefined') { console.warn('Hot-reload requires WebSocket support'); return; }
function connect() { try { hotReloadSocket = new WebSocket(url);
hotReloadSocket.onopen = () => { console.log('%c🔥 Hot-reload connected', 'color: #28a745;'); };
hotReloadSocket.onmessage = (event) => { try { const message = JSON.parse(event.data); handleHotReloadMessage(message); } catch (e) { // Ignore non-JSON messages } };
hotReloadSocket.onclose = () => { console.log('%c🔥 Hot-reload disconnected, reconnecting...', 'color: #ffc107;'); setTimeout(connect, reconnectDelay); };
hotReloadSocket.onerror = () => { hotReloadSocket.close(); }; } catch (e) { console.warn('Failed to connect to hot-reload server:', e); setTimeout(connect, reconnectDelay); } }
connect(); enableDevMode();}
/** * Handle hot-reload messages */function handleHotReloadMessage(message) { switch (message.command) { case 'reload': if (message.path?.endsWith('.css')) { reloadStyles(message.path); } else if (message.path?.endsWith('.js')) { reloadComponent(message.path); } else { // Full page reload location.reload(); } break;
case 'refresh-css': reloadAllStyles(); break;
case 'refresh-component': if (message.component) { reloadComponent(message.component); } break; }}
/** * Reload CSS styles */function reloadStyles(path) { const links = document.querySelectorAll('link[rel="stylesheet"]'); links.forEach(link => { if (link.href.includes(path)) { const url = new URL(link.href); url.searchParams.set('_reload', Date.now()); link.href = url.toString(); } });
console.log(`%c🔄 Reloaded styles: ${path}`, 'color: #17a2b8;');}
/** * Reload all styles */function reloadAllStyles() { const links = document.querySelectorAll('link[rel="stylesheet"]'); links.forEach(link => { const url = new URL(link.href); url.searchParams.set('_reload', Date.now()); link.href = url.toString(); });
// Also reload peek theme styles const peekStyles = document.querySelectorAll('style[data-peek-theme]'); peekStyles.forEach(style => { // Re-inject theme const theme = style.dataset.peekTheme; if (theme) { import('./theme.js').then(({ injectThemeCSS }) => { style.remove(); injectThemeCSS(theme, document); }); } });
console.log('%c🔄 Reloaded all styles', 'color: #17a2b8;');}
/** * Reload a component module * Note: Full module hot-reload requires native import.meta.hot support */async function reloadComponent(pathOrName) { // Extract component name from path const name = pathOrName.includes('/') ? pathOrName.split('/').pop().replace('.js', '') : pathOrName;
console.log(`%c🔄 Component changed: ${name}`, 'color: #17a2b8;');
// For now, suggest full reload - true HMR requires bundler support console.log('Full hot-reload for components requires page refresh or bundler HMR support');}
/** * Inspect a component * @param {string|HTMLElement} target - Component name or instance */export function inspectComponent(target) { let element; let name;
if (typeof target === 'string') { name = target; element = document.querySelector(target); } else { element = target; name = element?.tagName?.toLowerCase(); }
const def = getComponent(name); const state = loadState.get(name);
const info = { name, definition: def, loadState: state, element: element || null, properties: {}, attributes: {}, shadowRoot: null };
if (element) { // Get properties const proto = Object.getPrototypeOf(element); const propNames = Object.keys(element.constructor.properties || {}); propNames.forEach(prop => { info.properties[prop] = element[prop]; });
// Get attributes Array.from(element.attributes).forEach(attr => { info.attributes[attr.name] = attr.value; });
// Shadow root info if (element.shadowRoot) { info.shadowRoot = { mode: element.shadowRoot.mode, childCount: element.shadowRoot.childElementCount, styles: element.shadowRoot.querySelectorAll('style').length }; } }
console.group(`🔍 Component: ${name}`); console.log('Definition:', info.definition); console.log('Load state:', info.loadState); console.log('Properties:', info.properties); console.log('Attributes:', info.attributes); console.log('Shadow root:', info.shadowRoot); if (element) console.log('Element:', element); console.groupEnd();
return info;}
/** * List all components and their states */export function listComponents() { const names = registry.names(); const components = names.map(name => ({ name, state: loadState.get(name), defined: !!customElements.get(name), instances: document.querySelectorAll(name).length }));
console.table(components); return components;}
/** * Get development statistics */export function getStats() { return { version: version.current, components: registry.stats(), theme: getTheme(), extensions: getExtensionIds().length, devMode: isDevMode, hotReload: !!hotReloadSocket };}
/** * Performance timing for component rendering */const renderTimings = new Map();
/** * Start timing a component render */export function startTiming(componentName) { renderTimings.set(componentName, performance.now());}
/** * End timing and log result */export function endTiming(componentName) { const start = renderTimings.get(componentName); if (start) { const duration = performance.now() - start; renderTimings.delete(componentName);
if (duration > 16) { // Longer than one frame console.warn(`⚠️ Slow render: ${componentName} took ${duration.toFixed(2)}ms`); }
return duration; } return 0;}
/** * Component render profiler mixin */export function ProfilerMixin(Base) { return class extends Base { performUpdate() { startTiming(this.tagName.toLowerCase()); super.performUpdate(); endTiming(this.tagName.toLowerCase()); } };}
/** * Debug logger with component context */export function createLogger(componentName) { const prefix = `[${componentName}]`; return { log: (...args) => console.log(prefix, ...args), warn: (...args) => console.warn(prefix, ...args), error: (...args) => console.error(prefix, ...args), debug: (...args) => isDevMode && console.debug(prefix, ...args), group: (label) => console.group(`${prefix} ${label}`), groupEnd: () => console.groupEnd(), time: (label) => console.time(`${prefix} ${label}`), timeEnd: (label) => console.timeEnd(`${prefix} ${label}`) };}
/** * Dev tools object for console access */export const devTools = { enable: enableDevMode, hotReload: enableHotReload, inspect: inspectComponent, list: listComponents, stats: getStats, timing: { start: startTiming, end: endTiming }, logger: createLogger, ProfilerMixin};
export default devTools;