228 lines
8.1 KiB
JavaScript
228 lines
8.1 KiB
JavaScript
/**
|
|
* Preact Loader Utility
|
|
* =====================
|
|
* Provides a centralized, single-instance loader for Preact and related libraries.
|
|
* Ensures consistent loading from local vendor files with CDN fallback.
|
|
*
|
|
* Usage:
|
|
* import { loadPreact, isPreactEnabled } from './utils/preact-loader.js';
|
|
*
|
|
* if (isPreactEnabled()) {
|
|
* const { h, render } = await loadPreact();
|
|
* // ... use Preact
|
|
* }
|
|
*
|
|
* @module preact-loader
|
|
*/
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// CONFIGURATION
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
// Feature flag to enable/disable Preact islands
|
|
// Set to 'false' to completely disable all Preact functionality and use Vanilla JS fallbacks
|
|
const PREACT_ENABLED = true;
|
|
|
|
// Library versions for consistency
|
|
const VERSIONS = {
|
|
preact: '10.26.5',
|
|
signals: '1.3.1',
|
|
htm: '3.1.1'
|
|
};
|
|
|
|
// Local vendor paths (relative to web/)
|
|
const VENDOR_PATHS = {
|
|
preact: './lib/preact.module.js',
|
|
preactHooks: './lib/preact-hooks.module.js',
|
|
signals: './lib/preact-signals.module.js',
|
|
htm: './lib/htm.module.js'
|
|
};
|
|
|
|
// CDN fallback paths (used if local files fail)
|
|
const CDN_PATHS = {
|
|
preact: `https://esm.sh/preact@${VERSIONS.preact}`,
|
|
preactHooks: `https://esm.sh/preact@${VERSIONS.preact}/hooks`,
|
|
signals: `https://esm.sh/@preact/signals@${VERSIONS.signals}`,
|
|
htm: `https://esm.sh/htm@${VERSIONS.htm}`
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// STATE
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
// Cached loaded modules (single instance pattern)
|
|
let cachedPreact = null;
|
|
let cachedHooks = null;
|
|
let cachedSignals = null;
|
|
let cachedHtm = null;
|
|
let loadPromise = null;
|
|
let loadError = null;
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// PUBLIC API
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Check if Preact islands are enabled.
|
|
* Checks both the hardcoded flag and localStorage override.
|
|
* @returns {boolean} True if Preact is enabled
|
|
*/
|
|
export function isPreactEnabled() {
|
|
// Check localStorage override (user can disable via developer tools)
|
|
if (localStorage.getItem('doctor_preact_disabled') === 'true') {
|
|
return false;
|
|
}
|
|
return PREACT_ENABLED;
|
|
}
|
|
|
|
/**
|
|
* Check if Preact has been successfully loaded.
|
|
* @returns {boolean} True if Preact is loaded and ready
|
|
*/
|
|
export function isPreactLoaded() {
|
|
return cachedPreact !== null && loadError === null;
|
|
}
|
|
|
|
/**
|
|
* Get the last load error, if any.
|
|
* @returns {Error|null} The error object or null
|
|
*/
|
|
export function getLoadError() {
|
|
return loadError;
|
|
}
|
|
|
|
/**
|
|
* Load Preact and related libraries.
|
|
* Uses single-instance pattern to avoid multiple Preact instances.
|
|
*
|
|
* @returns {Promise<{h, render, Fragment, Component, createContext, useState, useEffect, useRef, useCallback, useMemo, signal, computed, effect, html}>}
|
|
*/
|
|
export async function loadPreact() {
|
|
// Return cached result if already loaded
|
|
if (cachedPreact && cachedHooks && cachedSignals && cachedHtm) {
|
|
return buildExports();
|
|
}
|
|
|
|
// Return existing promise if load is in progress
|
|
if (loadPromise) {
|
|
return loadPromise;
|
|
}
|
|
|
|
// Start loading
|
|
loadPromise = doLoad();
|
|
return loadPromise;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// INTERNAL FUNCTIONS
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Perform the actual loading of libraries.
|
|
*/
|
|
async function doLoad() {
|
|
loadError = null;
|
|
|
|
try {
|
|
// Try local vendor files first
|
|
const results = await Promise.allSettled([
|
|
tryImport(VENDOR_PATHS.preact, CDN_PATHS.preact, 'Preact'),
|
|
tryImport(VENDOR_PATHS.preactHooks, CDN_PATHS.preactHooks, 'Preact Hooks'),
|
|
tryImport(VENDOR_PATHS.signals, CDN_PATHS.signals, 'Preact Signals'),
|
|
tryImport(VENDOR_PATHS.htm, CDN_PATHS.htm, 'HTM')
|
|
]);
|
|
|
|
// Check for failures
|
|
const failures = results.filter(r => r.status === 'rejected');
|
|
if (failures.length > 0) {
|
|
const errorMessages = failures.map(f => f.reason?.message || 'Unknown error');
|
|
throw new Error(`Failed to load Preact libraries: ${errorMessages.join(', ')}`);
|
|
}
|
|
|
|
// Extract successful results
|
|
[cachedPreact, cachedHooks, cachedSignals, cachedHtm] = results.map(r => r.value);
|
|
|
|
console.log('[Preact-Loader] ✅ All libraries loaded successfully');
|
|
console.log('[Preact-Loader] Versions:', VERSIONS);
|
|
|
|
return buildExports();
|
|
} catch (error) {
|
|
loadError = error;
|
|
loadPromise = null; // Allow retry
|
|
console.error('[Preact-Loader] ❌ Failed to load:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Try importing a module from local path, fallback to CDN.
|
|
*/
|
|
async function tryImport(localPath, cdnPath, name) {
|
|
try {
|
|
// Try local first
|
|
const module = await import(localPath);
|
|
console.log(`[Preact-Loader] ✅ ${name} loaded from local vendor`);
|
|
return module;
|
|
} catch (localError) {
|
|
console.warn(`[Preact-Loader] ⚠️ ${name} local load failed, trying CDN...`, localError.message);
|
|
|
|
try {
|
|
const module = await import(cdnPath);
|
|
console.log(`[Preact-Loader] ✅ ${name} loaded from CDN`);
|
|
return module;
|
|
} catch (cdnError) {
|
|
console.error(`[Preact-Loader] ❌ ${name} CDN load also failed:`, cdnError.message);
|
|
throw new Error(`${name}: Local failed (${localError.message}), CDN failed (${cdnError.message})`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build the unified exports object.
|
|
*/
|
|
function buildExports() {
|
|
// Configure HTM to use Preact's h function
|
|
// HTM exports default function, not named 'htm'
|
|
const htmFn = cachedHtm.default || cachedHtm.htm || cachedHtm;
|
|
const html = htmFn.bind(cachedPreact.h);
|
|
|
|
return {
|
|
// Core Preact
|
|
h: cachedPreact.h,
|
|
render: cachedPreact.render,
|
|
Fragment: cachedPreact.Fragment,
|
|
Component: cachedPreact.Component,
|
|
createContext: cachedPreact.createContext,
|
|
|
|
// Hooks
|
|
useState: cachedHooks.useState,
|
|
useEffect: cachedHooks.useEffect,
|
|
useRef: cachedHooks.useRef,
|
|
useCallback: cachedHooks.useCallback,
|
|
useMemo: cachedHooks.useMemo,
|
|
useContext: cachedHooks.useContext,
|
|
useReducer: cachedHooks.useReducer,
|
|
|
|
// Signals
|
|
signal: cachedSignals.signal,
|
|
computed: cachedSignals.computed,
|
|
effect: cachedSignals.effect,
|
|
batch: cachedSignals.batch,
|
|
|
|
// HTM (tagged template literal for JSX-like syntax)
|
|
html
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reset the loader state (for testing or recovery).
|
|
*/
|
|
export function resetLoader() {
|
|
cachedPreact = null;
|
|
cachedHooks = null;
|
|
cachedSignals = null;
|
|
cachedHtm = null;
|
|
loadPromise = null;
|
|
loadError = null;
|
|
}
|