/** * ModuleAssetLoader * * 모듈/플러그인의 에셋(JS, CSS)을 동적으로 로드하는 클래스입니다. * TemplateApp 초기화 시 window.G7Config.moduleAssets를 기반으로 * 활성화된 모듈의 에셋을 로드합니다. */ import { createLogger } from '../utils/Logger'; import { loadScriptWithRetry } from '../template-engine/networkResilience'; const logger = createLogger('ModuleAssetLoader'); /** * 모듈 에셋 정보 인터페이스 */ export interface ModuleAsset { /** 모듈 식별자 (vendor-module 형식) */ identifier: string; /** JS 번들 URL */ js?: string; /** CSS 번들 URL */ css?: string; /** 로드 우선순위 (낮을수록 먼저) */ priority: number; /** 외부 스크립트 정의 (조건부 로드용) */ external?: ExternalScript[]; } /** * 외부 스크립트 정의 인터페이스 */ export interface ExternalScript { /** 스크립트 URL */ src: string; /** 스크립트 ID (중복 로드 방지) */ id: string; /** 조건부 로드 표현식 (예: "{{_global.settings.useLib}}") */ if?: string; } /** * 로드된 에셋 정보 */ interface LoadedAsset { /** 에셋 타입 (js, css) */ type: 'js' | 'css'; /** DOM 요소 */ element: HTMLElement; } /** * ModuleAssetLoader 클래스 * * 모듈 에셋의 동적 로드/언로드를 관리합니다. */ export class ModuleAssetLoader { /** 로드된 에셋 맵 (identifier -> LoadedAsset[]) */ private loadedAssets: Map = new Map(); /** 로드 중인 프로미스 맵 (중복 로드 방지) */ private loadingPromises: Map> = new Map(); /** * 로드에 최종 실패한 JS 번들/확장 식별자 집합 * * 실패가 확정된 확장의 핸들러는 영원히 등록되지 않는다. `waitForHandlers` 가 * 이 집합을 보고 오지 않을 핸들러를 기다리지 않도록 하기 위한 사실 기록이다. * * @since engine-v1.53.0 */ private failedJsAssets: Set = new Set(); /** * JS 에셋 로드에 최종 실패한 확장이 하나라도 있는지 반환합니다. * * @return bool 실패한 JS 번들/확장이 있으면 true * @since engine-v1.53.0 */ hasFailedJsAssets(): boolean { return this.failedJsAssets.size > 0; } /** * JS 에셋 로드에 최종 실패한 식별자 목록을 반환합니다. * * @return string[] 실패한 번들 키/확장 식별자 목록 * @since engine-v1.53.0 */ getFailedJsAssets(): string[] { return [...this.failedJsAssets]; } /** * 활성화된 모듈들의 에셋을 로드합니다. * * CSS/JS 모두 병렬 fetch로 로드합니다. JS는 `script.async = false` + * 정렬된 DOM append 순서로 **실행 순서는 priority 정렬대로** 보장됩니다. * (HTML 사양: async=false 스크립트는 삽입 순서대로 실행) * * 확장 하나의 로드 실패가 나머지 확장을 함께 죽이지 않도록 `allSettled` 로 모은다. * 실패한 확장은 `failedJsAssets` 에 기록되어 `waitForHandlers` 가 오지 않을 * 핸들러를 기다리지 않게 한다. (개별 로딩은 확장별로 독립이므로 부분 열화가 옳다. * 반면 병합 번들은 확장 전체가 한 파일이라 실패 시 reject 로 표면화한다.) * * @param extensions 모듈 에셋 배열 * @return Promise */ async loadActiveExtensionAssets(extensions: ModuleAsset[]): Promise { if (!extensions || extensions.length === 0) { logger.log('No module assets to load'); return; } // 우선순위 순으로 정렬 (낮을수록 먼저) const sortedExtensions = [...extensions].sort((a, b) => a.priority - b.priority); logger.log('Loading module assets:', sortedExtensions.map(e => e.identifier)); // CSS 병렬 로드 (렌더링 블로킹 방지) const cssPromises = sortedExtensions .filter(ext => ext.css) .map(ext => this.loadCSS(ext.identifier, ext.css!)); // JS 병렬 fetch (script.async=false로 실행 순서는 append 순서대로 유지) // 정렬된 순서로 순차 append하여 우선순위 기반 실행 순서를 보장 const jsPromises = sortedExtensions .filter(ext => ext.js) .map(ext => this.loadJS(ext.identifier, ext.js!)); const results = await Promise.allSettled([...cssPromises, ...jsPromises]); const failures = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected'); if (failures.length > 0) { logger.warn( `Some module assets failed to load (${failures.length}/${results.length}); continuing with the rest`, this.getFailedJsAssets() ); return; } logger.log('All module assets loaded successfully'); } /** * 서버측에서 병합된 확장 번들(JS/CSS)을 로드합니다. * * 활성 모듈/플러그인 IIFE 를 priority 순으로 이어붙인 단일 파일을 하나의 * `