experiments in a post-browser web
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;