experiments in a post-browser web
at main 371 lines 9.2 kB view raw
1/** 2 * Peek Development Utilities 3 * 4 * Hot-reload, debugging, and development helpers. 5 * Only import in development - tree-shake in production. 6 * 7 * Usage: 8 * import { enableHotReload, devTools } from 'peek://app/components/dev.js'; 9 * 10 * enableHotReload(); // Connect to dev server 11 * devTools.inspect('peek-button'); // Inspect component 12 */ 13 14import { registry, getComponent, loadState } from './registry.js'; 15import { getTheme, getThemeTokens, getThemeNames } from './theme.js'; 16import { getExtensionIds, getExtension } from './extension.js'; 17import { version } from './version.js'; 18 19// Dev mode state 20let isDevMode = false; 21let hotReloadSocket = null; 22const componentInstances = new WeakMap(); 23 24/** 25 * Enable development mode 26 */ 27export function enableDevMode() { 28 isDevMode = true; 29 30 // Add global debug object 31 if (typeof window !== 'undefined') { 32 window.__PEEK_DEV__ = { 33 registry, 34 theme: { 35 current: getTheme, 36 tokens: getThemeTokens, 37 names: getThemeNames 38 }, 39 extensions: { 40 ids: getExtensionIds, 41 get: getExtension 42 }, 43 version, 44 inspect: inspectComponent, 45 listComponents: listComponents, 46 stats: getStats 47 }; 48 49 console.log( 50 '%c🔍 Peek Dev Mode Enabled', 51 'background: #007aff; color: white; padding: 4px 8px; border-radius: 4px;' 52 ); 53 console.log('Access dev tools via window.__PEEK_DEV__'); 54 } 55} 56 57/** 58 * Check if dev mode is enabled 59 */ 60export function isDevModeEnabled() { 61 return isDevMode; 62} 63 64/** 65 * Enable hot-reload connection 66 * @param {Object} options - Hot reload options 67 * @param {string} options.url - WebSocket server URL 68 * @param {number} options.reconnectDelay - Reconnection delay in ms 69 */ 70export function enableHotReload(options = {}) { 71 const { 72 url = 'ws://localhost:35729/livereload', 73 reconnectDelay = 1000 74 } = options; 75 76 if (typeof WebSocket === 'undefined') { 77 console.warn('Hot-reload requires WebSocket support'); 78 return; 79 } 80 81 function connect() { 82 try { 83 hotReloadSocket = new WebSocket(url); 84 85 hotReloadSocket.onopen = () => { 86 console.log('%c🔥 Hot-reload connected', 'color: #28a745;'); 87 }; 88 89 hotReloadSocket.onmessage = (event) => { 90 try { 91 const message = JSON.parse(event.data); 92 handleHotReloadMessage(message); 93 } catch (e) { 94 // Ignore non-JSON messages 95 } 96 }; 97 98 hotReloadSocket.onclose = () => { 99 console.log('%c🔥 Hot-reload disconnected, reconnecting...', 'color: #ffc107;'); 100 setTimeout(connect, reconnectDelay); 101 }; 102 103 hotReloadSocket.onerror = () => { 104 hotReloadSocket.close(); 105 }; 106 } catch (e) { 107 console.warn('Failed to connect to hot-reload server:', e); 108 setTimeout(connect, reconnectDelay); 109 } 110 } 111 112 connect(); 113 enableDevMode(); 114} 115 116/** 117 * Handle hot-reload messages 118 */ 119function handleHotReloadMessage(message) { 120 switch (message.command) { 121 case 'reload': 122 if (message.path?.endsWith('.css')) { 123 reloadStyles(message.path); 124 } else if (message.path?.endsWith('.js')) { 125 reloadComponent(message.path); 126 } else { 127 // Full page reload 128 location.reload(); 129 } 130 break; 131 132 case 'refresh-css': 133 reloadAllStyles(); 134 break; 135 136 case 'refresh-component': 137 if (message.component) { 138 reloadComponent(message.component); 139 } 140 break; 141 } 142} 143 144/** 145 * Reload CSS styles 146 */ 147function reloadStyles(path) { 148 const links = document.querySelectorAll('link[rel="stylesheet"]'); 149 links.forEach(link => { 150 if (link.href.includes(path)) { 151 const url = new URL(link.href); 152 url.searchParams.set('_reload', Date.now()); 153 link.href = url.toString(); 154 } 155 }); 156 157 console.log(`%c🔄 Reloaded styles: ${path}`, 'color: #17a2b8;'); 158} 159 160/** 161 * Reload all styles 162 */ 163function reloadAllStyles() { 164 const links = document.querySelectorAll('link[rel="stylesheet"]'); 165 links.forEach(link => { 166 const url = new URL(link.href); 167 url.searchParams.set('_reload', Date.now()); 168 link.href = url.toString(); 169 }); 170 171 // Also reload peek theme styles 172 const peekStyles = document.querySelectorAll('style[data-peek-theme]'); 173 peekStyles.forEach(style => { 174 // Re-inject theme 175 const theme = style.dataset.peekTheme; 176 if (theme) { 177 import('./theme.js').then(({ injectThemeCSS }) => { 178 style.remove(); 179 injectThemeCSS(theme, document); 180 }); 181 } 182 }); 183 184 console.log('%c🔄 Reloaded all styles', 'color: #17a2b8;'); 185} 186 187/** 188 * Reload a component module 189 * Note: Full module hot-reload requires native import.meta.hot support 190 */ 191async function reloadComponent(pathOrName) { 192 // Extract component name from path 193 const name = pathOrName.includes('/') 194 ? pathOrName.split('/').pop().replace('.js', '') 195 : pathOrName; 196 197 console.log(`%c🔄 Component changed: ${name}`, 'color: #17a2b8;'); 198 199 // For now, suggest full reload - true HMR requires bundler support 200 console.log('Full hot-reload for components requires page refresh or bundler HMR support'); 201} 202 203/** 204 * Inspect a component 205 * @param {string|HTMLElement} target - Component name or instance 206 */ 207export function inspectComponent(target) { 208 let element; 209 let name; 210 211 if (typeof target === 'string') { 212 name = target; 213 element = document.querySelector(target); 214 } else { 215 element = target; 216 name = element?.tagName?.toLowerCase(); 217 } 218 219 const def = getComponent(name); 220 const state = loadState.get(name); 221 222 const info = { 223 name, 224 definition: def, 225 loadState: state, 226 element: element || null, 227 properties: {}, 228 attributes: {}, 229 shadowRoot: null 230 }; 231 232 if (element) { 233 // Get properties 234 const proto = Object.getPrototypeOf(element); 235 const propNames = Object.keys(element.constructor.properties || {}); 236 propNames.forEach(prop => { 237 info.properties[prop] = element[prop]; 238 }); 239 240 // Get attributes 241 Array.from(element.attributes).forEach(attr => { 242 info.attributes[attr.name] = attr.value; 243 }); 244 245 // Shadow root info 246 if (element.shadowRoot) { 247 info.shadowRoot = { 248 mode: element.shadowRoot.mode, 249 childCount: element.shadowRoot.childElementCount, 250 styles: element.shadowRoot.querySelectorAll('style').length 251 }; 252 } 253 } 254 255 console.group(`🔍 Component: ${name}`); 256 console.log('Definition:', info.definition); 257 console.log('Load state:', info.loadState); 258 console.log('Properties:', info.properties); 259 console.log('Attributes:', info.attributes); 260 console.log('Shadow root:', info.shadowRoot); 261 if (element) console.log('Element:', element); 262 console.groupEnd(); 263 264 return info; 265} 266 267/** 268 * List all components and their states 269 */ 270export function listComponents() { 271 const names = registry.names(); 272 const components = names.map(name => ({ 273 name, 274 state: loadState.get(name), 275 defined: !!customElements.get(name), 276 instances: document.querySelectorAll(name).length 277 })); 278 279 console.table(components); 280 return components; 281} 282 283/** 284 * Get development statistics 285 */ 286export function getStats() { 287 return { 288 version: version.current, 289 components: registry.stats(), 290 theme: getTheme(), 291 extensions: getExtensionIds().length, 292 devMode: isDevMode, 293 hotReload: !!hotReloadSocket 294 }; 295} 296 297/** 298 * Performance timing for component rendering 299 */ 300const renderTimings = new Map(); 301 302/** 303 * Start timing a component render 304 */ 305export function startTiming(componentName) { 306 renderTimings.set(componentName, performance.now()); 307} 308 309/** 310 * End timing and log result 311 */ 312export function endTiming(componentName) { 313 const start = renderTimings.get(componentName); 314 if (start) { 315 const duration = performance.now() - start; 316 renderTimings.delete(componentName); 317 318 if (duration > 16) { // Longer than one frame 319 console.warn(`⚠️ Slow render: ${componentName} took ${duration.toFixed(2)}ms`); 320 } 321 322 return duration; 323 } 324 return 0; 325} 326 327/** 328 * Component render profiler mixin 329 */ 330export function ProfilerMixin(Base) { 331 return class extends Base { 332 performUpdate() { 333 startTiming(this.tagName.toLowerCase()); 334 super.performUpdate(); 335 endTiming(this.tagName.toLowerCase()); 336 } 337 }; 338} 339 340/** 341 * Debug logger with component context 342 */ 343export function createLogger(componentName) { 344 const prefix = `[${componentName}]`; 345 return { 346 log: (...args) => console.log(prefix, ...args), 347 warn: (...args) => console.warn(prefix, ...args), 348 error: (...args) => console.error(prefix, ...args), 349 debug: (...args) => isDevMode && console.debug(prefix, ...args), 350 group: (label) => console.group(`${prefix} ${label}`), 351 groupEnd: () => console.groupEnd(), 352 time: (label) => console.time(`${prefix} ${label}`), 353 timeEnd: (label) => console.timeEnd(`${prefix} ${label}`) 354 }; 355} 356 357/** 358 * Dev tools object for console access 359 */ 360export const devTools = { 361 enable: enableDevMode, 362 hotReload: enableHotReload, 363 inspect: inspectComponent, 364 list: listComponents, 365 stats: getStats, 366 timing: { start: startTiming, end: endTiming }, 367 logger: createLogger, 368 ProfilerMixin 369}; 370 371export default devTools;