Files
newznab-tmux-NNTmux/resources/js/alpine/lazy-loader.js
T
2026-07-11 11:40:39 +02:00

120 lines
5.2 KiB
JavaScript

/**
* Alpine.js CSP-safe lazy component loader.
*
* Scans the DOM for x-data attributes that match known lazy components.
* Only imports the JS modules for components actually present on the page.
* After all needed modules are loaded, calls Alpine.start().
*
* Dynamic import() is CSP-compliant (no eval / new Function).
*/
import Alpine from '@alpinejs/csp';
/**
* Map of Alpine component name → dynamic import function.
* Each module registers itself via Alpine.data() as a side effect.
*/
const lazyComponentMap = {
// --- Components only needed on specific pages ---
'adminSubmenu': () => import('./components/admin-submenu.js'),
'passwordToggle': () => import('./components/password-toggle.js'),
// --- Modal components (only on release pages) ---
'nfoModal': () => import('./components/nfo-modal.js'),
'filelistModal': () => import('./components/filelist-modal.js'),
'previewModal': () => import('./components/preview-modal.js'),
'mediainfoModal': () => import('./components/mediainfo-modal.js'),
'imageModal': () => import('./components/image-modal.js'),
// --- Page-specific components ---
'moviesPage': () => import('./components/movies-page.js'),
'qualityFilter': () => import('./components/quality-filter.js'),
'contentToggle': () => import('./components/content-toggle.js'),
'contentDelete': () => import('./components/content-toggle.js'), // same file
'releaseReport': () => import('./components/release-report.js'),
'adminReleaseReports': () => import('./components/release-report.js'), // same file
'adminReleaseList': () => import('./components/admin/release-list.js'),
'showAddForm': () => import('./components/admin/show-add.js'),
'profileEdit': () => import('./components/profile-edit.js'),
'profilePage': () => import('./components/profile-edit.js'), // same file
'copyToClipboard': () => import('./components/profile-edit.js'), // same file
'cartPage': () => import('./components/cart-page.js'),
'releaseMultiOps': () => import('./components/cart-page.js'), // same file
'seriesSeasonLoader': () => import('./components/series-season-loader.js'),
'authPage': () => import('./components/auth-page.js'),
'otpInput': () => import('./components/auth-page.js'), // same file
'loginMode': () => import('./components/login-mode.js'),
'passkeyLogin': () => import('./components/passkey-login.js'),
'passkeyManage': () => import('./components/passkey-manage.js'),
'adminUserPasskeys': () => import('./components/admin-user-passkeys.js'),
// --- Admin components (includes Chart.js — heavy) ---
'adminDashboard': () => import('./components/admin/dashboard.js'),
'adminGroups': () => import('./components/admin/groups.js'),
'adminFeatures': () => import('./components/admin/features.js'),
'adminUserEdit': () => import('./components/admin/features.js'), // same file
'adminUserList': () => import('./components/admin/user-list.js'),
'adminDeletedUsers': () => import('./components/admin/features.js'), // same file
'adminInvitations': () => import('./components/admin/features.js'), // same file
'adminRegexForm': () => import('./components/admin/features.js'), // same file
'tinyMceEditor': () => import('./components/admin/features.js'), // same file
'verifyUser': () => import('./components/admin/features.js'), // same file
'tmuxEdit': () => import('./components/admin/features.js'), // same file
};
/**
* Extract Alpine component names from all x-data attributes in the DOM.
* Handles: x-data="componentName", x-data="componentName()", x-data="{ ... }" (skipped).
*/
function getUsedComponentNames() {
const names = new Set();
document.querySelectorAll('[x-data]').forEach(el => {
const raw = (el.getAttribute('x-data') || '').trim();
// Skip inline objects like { show: true }
if (!raw || raw.startsWith('{')) return;
// Extract identifier before ( or whitespace
const match = raw.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
if (match) names.add(match[1]);
});
return names;
}
/**
* Load only the lazy components found on the current page, then start Alpine.
*/
export function loadAndStart() {
const usedNames = getUsedComponentNames();
const imports = new Set(); // deduplicate (multiple names → same file)
for (const name of usedNames) {
if (lazyComponentMap[name]) {
imports.add(lazyComponentMap[name]);
}
}
if (imports.size === 0) {
Alpine.start();
enableTransitions();
return;
}
// Load all needed modules in parallel, then start Alpine
Promise.all([...imports].map(fn => fn()))
.then(() => {
Alpine.start();
enableTransitions();
})
.catch(err => {
console.error('[lazy-loader] Failed to load component:', err);
Alpine.start();
enableTransitions();
});
}
function enableTransitions() {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document.documentElement.removeAttribute('data-loading');
});
});
}