LiveReload JavaScript client that enables automatic browser refreshing and live CSS reloading during development
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Plugin system for extending LiveReload to handle custom file types and processing workflows. Plugins can implement custom reload strategies, integrate with external tools, and provide specialized handling for different development environments.
All LiveReload plugins must implement a specific interface with static properties and instance methods.
/**
* Base plugin interface that all LiveReload plugins must implement
*/
class LiveReloadPlugin {
/** Unique plugin identifier (required static property) */
static identifier = 'plugin-name';
/** Plugin version in x.y or x.y.z format (required static property) */
static version = '1.0.0';
/**
* Plugin constructor called when plugin is registered
* @param window - Browser window object
* @param host - Plugin host API object with LiveReload utilities
*/
constructor(window, host) {}
/**
* Attempt to reload the given file path (optional method)
* @param path - File path that changed
* @param options - Reload options object
* @returns True if plugin handled the reload, false otherwise
*/
reload(path, options) {}
/**
* Provide plugin-specific analysis data (optional method)
* @returns Object with plugin information for the server
*/
analyze() {}
}Usage Examples:
// Basic plugin implementation
class MyFileTypePlugin {
static identifier = 'my-file-type';
static version = '1.0.0';
constructor(window, host) {
this.window = window;
this.host = host;
this.console = host.console;
}
reload(path, options) {
if (path.endsWith('.myext')) {
this.console.log('Reloading custom file:', path);
// Implement custom reload logic
this.reloadCustomFile(path);
return true; // Indicate we handled this file
}
return false; // Let other plugins handle it
}
reloadCustomFile(path) {
// Custom implementation
this.window.location.reload();
}
}
// Register the plugin
LiveReload.addPlugin(MyFileTypePlugin);The host object provides access to LiveReload's internal APIs and utilities for plugin development.
/**
* Plugin host API object passed to plugin constructors
*/
interface PluginHost {
/** Console logging that respects LiveReload's debug settings */
console: {
log(message: string): void;
error(message: string): void;
};
/** Timer utility class for scheduling operations */
Timer: typeof Timer;
/** Generate cache-busting URLs for resources */
generateCacheBustUrl(url: string): string;
/** Internal LiveReload instance (private API, subject to change) */
_livereload: LiveReload;
/** Internal Reloader instance (private API, subject to change) */
_reloader: Reloader;
/** Internal Connector instance (private API, subject to change) */
_connector: Connector;
}Usage Examples:
class AdvancedPlugin {
static identifier = 'advanced';
static version = '2.1.0';
constructor(window, host) {
this.window = window;
this.host = host;
// Use official APIs
this.console = host.console;
this.Timer = host.Timer;
// Set up timer for periodic operations
this.updateTimer = new this.Timer(() => {
this.checkForUpdates();
});
}
reload(path, options) {
if (path.match(/\.(scss|sass)$/)) {
this.console.log('Processing Sass file:', path);
// Generate cache-busting URL
const cacheBustedUrl = this.host.generateCacheBustUrl(path);
// Custom Sass processing
return this.processSassFile(cacheBustedUrl);
}
return false;
}
processSassFile(url) {
// Custom implementation
return true;
}
checkForUpdates() {
this.console.log('Checking for plugin updates');
}
}Example of a real plugin implementation that handles LESS stylesheet reloading.
/**
* Built-in LESS plugin for handling LESS stylesheet compilation
*/
class LessPlugin {
static identifier = 'less';
static version = '1.0';
constructor(window, host) {
this.window = window;
this.host = host;
}
/**
* Handle LESS file reloading by triggering LESS.js recompilation
* @param path - File path that changed
* @param options - Reload options
* @returns True if LESS file was handled, false otherwise
*/
reload(path, options) {
if (this.window.less && this.window.less.refresh) {
if (path.match(/\.less$/i) || options.originalPath.match(/\.less$/i)) {
return this.reloadLess(path);
}
}
return false;
}
/**
* Reload LESS stylesheets by updating link hrefs and triggering recompilation
* @param path - LESS file path that changed
* @returns True if reloading was performed, false if no LESS links found
*/
reloadLess(path) {
const links = Array.from(document.getElementsByTagName('link')).filter(link =>
(link.href && link.rel.match(/^stylesheet\/less$/i)) ||
(link.rel.match(/stylesheet/i) && link.type.match(/^text\/(x-)?less$/i))
);
if (links.length === 0) return false;
for (const link of links) {
link.href = this.host.generateCacheBustUrl(link.href);
}
this.host.console.log('LiveReload is asking LESS to recompile all stylesheets');
this.window.less.refresh(true);
return true;
}
/**
* Provide analysis data about LESS.js availability
* @returns Object indicating if LESS compilation should be disabled server-side
*/
analyze() {
return {
disable: !!(this.window.less && this.window.less.refresh)
};
}
}Usage Examples:
// The LESS plugin is automatically registered by LiveReload
// But you can check if it's available:
if (LiveReload.hasPlugin('less')) {
console.log('LESS plugin is active');
// LESS.js will handle .less file recompilation automatically
// when files change on the server
}Practical examples of custom plugins for different use cases.
Usage Examples:
// React component hot reloading plugin
class ReactHotReloadPlugin {
static identifier = 'react-hot';
static version = '1.0.0';
constructor(window, host) {
this.window = window;
this.host = host;
}
reload(path, options) {
if (path.match(/\.jsx?$/) && this.window.React) {
this.host.console.log('Hot reloading React component:', path);
// Custom React hot reload logic
if (this.window.__REACT_HOT_LOADER__) {
this.window.__REACT_HOT_LOADER__.reload();
return true;
}
}
return false;
}
analyze() {
return {
reactVersion: this.window.React ? this.window.React.version : null,
hotReloadAvailable: !!this.window.__REACT_HOT_LOADER__
};
}
}
// TypeScript compilation plugin
class TypeScriptPlugin {
static identifier = 'typescript';
static version = '1.0.0';
constructor(window, host) {
this.window = window;
this.host = host;
this.pendingCompilation = false;
}
reload(path, options) {
if (path.match(/\.ts$/) && !this.pendingCompilation) {
this.host.console.log('TypeScript file changed:', path);
this.pendingCompilation = true;
// Trigger compilation and reload
this.compileTypeScript(path).then(() => {
this.pendingCompilation = false;
this.window.location.reload();
});
return true;
}
return false;
}
async compileTypeScript(path) {
// Custom TypeScript compilation logic
this.host.console.log('Compiling TypeScript...');
// Implementation would go here
}
}
// Image optimization plugin
class ImageOptimizationPlugin {
static identifier = 'image-optimizer';
static version = '1.0.0';
constructor(window, host) {
this.window = window;
this.host = host;
}
reload(path, options) {
const imageRegex = /\.(jpe?g|png|gif|svg|webp)$/i;
if (path.match(imageRegex)) {
this.host.console.log('Optimizing and reloading image:', path);
// Custom image handling with optimization
this.optimizeAndReload(path);
return true;
}
return false;
}
optimizeAndReload(path) {
// Generate optimized cache-busting URL
const optimizedUrl = this.host.generateCacheBustUrl(path) + '&optimize=true';
// Update all image references
const images = this.window.document.querySelectorAll('img');
images.forEach(img => {
if (img.src.includes(path)) {
img.src = optimizedUrl;
}
});
}
analyze() {
const images = this.window.document.querySelectorAll('img');
return {
imageCount: images.length,
optimizationEnabled: true
};
}
}
// Register custom plugins
LiveReload.addPlugin(ReactHotReloadPlugin);
LiveReload.addPlugin(TypeScriptPlugin);
LiveReload.addPlugin(ImageOptimizationPlugin);How plugins are automatically detected and registered with LiveReload.
/**
* Automatic plugin registration for global plugins
* LiveReload scans window object for properties matching 'LiveReloadPlugin*'
*/
window.LiveReloadPluginMyCustom = MyCustomPlugin;
/**
* Manual plugin registration
* Register plugin classes directly with LiveReload instance
*/
LiveReload.addPlugin(MyCustomPlugin);Usage Examples:
// Automatic registration - plugin will be auto-detected
window.LiveReloadPluginSass = SassPlugin;
window.LiveReloadPluginVue = VuePlugin;
// Manual registration - explicit control
LiveReload.addPlugin(MyCustomPlugin);
LiveReload.addPlugin(AnotherPlugin);
// Check if plugins are registered
if (LiveReload.hasPlugin('sass')) {
console.log('Sass plugin detected and registered');
}
// Conditional plugin registration
if (window.Vue) {
LiveReload.addPlugin(VuePlugin);
}Guidelines for creating effective and reliable LiveReload plugins.
Best Practices:
host.console for consistent logging behaviorUsage Examples:
class WellDesignedPlugin {
static identifier = 'company-toolname'; // Unique and descriptive
static version = '2.1.3'; // Semantic versioning
constructor(window, host) {
this.window = window;
this.host = host;
// Check for required dependencies
this.isEnabled = this.checkDependencies();
if (this.isEnabled) {
this.host.console.log('WellDesignedPlugin initialized successfully');
} else {
this.host.console.log('WellDesignedPlugin disabled - missing dependencies');
}
}
checkDependencies() {
// Graceful dependency checking
return !!(this.window.RequiredLibrary && this.window.RequiredLibrary.version);
}
reload(path, options) {
// Early return if disabled
if (!this.isEnabled) {
return false;
}
try {
if (this.shouldHandle(path, options)) {
this.host.console.log(`Processing ${path}`);
// Non-blocking async operation
this.handleReload(path, options).catch(error => {
this.host.console.error(`Error processing ${path}: ${error.message}`);
});
return true;
}
} catch (error) {
this.host.console.error(`Plugin error: ${error.message}`);
}
return false;
}
shouldHandle(path, options) {
// Clear logic for when to handle files
return path.match(/\.customext$/) && !options.skipCustom;
}
async handleReload(path, options) {
// Async processing without blocking
const result = await this.processFile(path);
if (result.success) {
this.applyChanges(result.data);
}
}
analyze() {
// Provide useful analysis data
return {
enabled: this.isEnabled,
version: this.constructor.version,
dependencies: {
RequiredLibrary: this.window.RequiredLibrary ?
this.window.RequiredLibrary.version : null
}
};
}
}