feat(services): introduce DeesServiceLibLoader to lazy-load heavy client libraries from CDN and update components to use it
This commit is contained in:
10
changelog.md
10
changelog.md
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-01-01 - 3.27.0 - feat(services)
|
||||||
|
introduce DeesServiceLibLoader to lazy-load heavy client libraries from CDN and update components to use it
|
||||||
|
|
||||||
|
- Add DeesServiceLibLoader singleton (ts_web/services/DeesServiceLibLoader.ts) to lazily load and cache libraries via jsDelivr ESM: xterm, xterm-addon-fit, highlight.js, ApexCharts, and Tiptap.
|
||||||
|
- Inject xterm CSS dynamically to avoid shipping xterm styles in the initial bundle.
|
||||||
|
- Expose helper methods preloadAll() and isLoaded(), and typed bundle interfaces (IXtermBundle, IXtermFitAddonBundle, ITiptapBundle).
|
||||||
|
- Update components to use runtime-loaded modules: dees-chart-area, dees-dataview-codebox, dees-input-richtext, wysiwyg code block, dees-workspace-terminal, terminal-tab-manager, dees-workspace-terminal-preview.
|
||||||
|
- TerminalTabManager now requires setXtermModules(...) before creating tabs and will throw if not initialized; workspace terminal now initializes and passes the loaded modules.
|
||||||
|
- Replace direct runtime imports of heavy libs with typed imports and runtime-loaded bundles to reduce initial bundle size and improve load performance.
|
||||||
|
|
||||||
## 2026-01-01 - 3.26.1 - fix(dees-actionbar)
|
## 2026-01-01 - 3.26.1 - fix(dees-actionbar)
|
||||||
animate actionbar hide using grid-template-rows and wait for animation before clearing state
|
animate actionbar hide using grid-template-rows and wait for animation before clearing state
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '3.26.1',
|
version: '3.27.0',
|
||||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { demoFunc } from './demo.js';
|
|||||||
import { chartAreaStyles } from './styles.js';
|
import { chartAreaStyles } from './styles.js';
|
||||||
import { renderChartArea } from './template.js';
|
import { renderChartArea } from './template.js';
|
||||||
|
|
||||||
import ApexCharts from 'apexcharts';
|
import type ApexCharts from 'apexcharts';
|
||||||
|
import { DeesServiceLibLoader } from '../../../services/index.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -150,7 +151,10 @@ export class DeesChartArea extends DeesElement {
|
|||||||
|
|
||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
await this.domtoolsPromise;
|
await this.domtoolsPromise;
|
||||||
|
|
||||||
|
// Load ApexCharts from CDN
|
||||||
|
const ApexChartsLib = await DeesServiceLibLoader.getInstance().loadApexCharts();
|
||||||
|
|
||||||
// Wait for next animation frame to ensure layout is complete
|
// Wait for next animation frame to ensure layout is complete
|
||||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||||
|
|
||||||
@@ -353,7 +357,7 @@ export class DeesChartArea extends DeesElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.chart = new ApexCharts(this.shadowRoot.querySelector('.chartContainer'), options);
|
this.chart = new ApexChartsLib(this.shadowRoot.querySelector('.chartContainer'), options);
|
||||||
await this.chart.render();
|
await this.chart.render();
|
||||||
|
|
||||||
// Give the chart a moment to fully initialize before resizing
|
// Give the chart a moment to fully initialize before resizing
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import {
|
|||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import { cssGeistFontFamily, cssMonoFontFamily } from '../../00fonts.js';
|
import { cssGeistFontFamily, cssMonoFontFamily } from '../../00fonts.js';
|
||||||
|
|
||||||
import hlight from 'highlight.js';
|
import type { HLJSApi } from 'highlight.js';
|
||||||
|
|
||||||
import * as smartstring from '@push.rocks/smartstring';
|
import * as smartstring from '@push.rocks/smartstring';
|
||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
|
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
|
||||||
|
import { DeesServiceLibLoader } from '../../../services/index.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -229,6 +230,7 @@ export class DeesDataviewCodebox extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private codeToDisplayStore = '';
|
private codeToDisplayStore = '';
|
||||||
|
private highlightJs: HLJSApi | null = null;
|
||||||
|
|
||||||
public async updated(_changedProperties) {
|
public async updated(_changedProperties) {
|
||||||
super.updated(_changedProperties);
|
super.updated(_changedProperties);
|
||||||
@@ -250,11 +252,17 @@ export class DeesDataviewCodebox extends DeesElement {
|
|||||||
this.codeToDisplay = this.codeToDisplayStore;
|
this.codeToDisplay = this.codeToDisplayStore;
|
||||||
}
|
}
|
||||||
await domtools.plugins.smartdelay.delayFor(0);
|
await domtools.plugins.smartdelay.delayFor(0);
|
||||||
|
|
||||||
|
// Load highlight.js from CDN if not already loaded
|
||||||
|
if (!this.highlightJs) {
|
||||||
|
this.highlightJs = await DeesServiceLibLoader.getInstance().loadHighlightJs();
|
||||||
|
}
|
||||||
|
|
||||||
const localCodeNode = this.shadowRoot.querySelector('code');
|
const localCodeNode = this.shadowRoot.querySelector('code');
|
||||||
const html = hlight.highlight(this.codeToDisplayStore, {
|
const highlightedHtml = this.highlightJs.highlight(this.codeToDisplayStore, {
|
||||||
language: this.progLang,
|
language: this.progLang,
|
||||||
ignoreIllegals: true,
|
ignoreIllegals: true,
|
||||||
});
|
});
|
||||||
localCodeNode.innerHTML = html.value;
|
localCodeNode.innerHTML = highlightedHtml.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,8 @@ import {
|
|||||||
query,
|
query,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import { Editor } from '@tiptap/core';
|
import type { Editor } from '@tiptap/core';
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
import { DeesServiceLibLoader, type ITiptapBundle } from '../../../services/index.js';
|
||||||
import Underline from '@tiptap/extension-underline';
|
|
||||||
import TextAlign from '@tiptap/extension-text-align';
|
|
||||||
import Link from '@tiptap/extension-link';
|
|
||||||
import Typography from '@tiptap/extension-typography';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -63,6 +59,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
|||||||
|
|
||||||
private editorElement: HTMLElement;
|
private editorElement: HTMLElement;
|
||||||
private linkInputElement: HTMLInputElement;
|
private linkInputElement: HTMLInputElement;
|
||||||
|
private tiptapBundle: ITiptapBundle | null = null;
|
||||||
|
|
||||||
public editor: Editor;
|
public editor: Editor;
|
||||||
|
|
||||||
@@ -233,13 +230,19 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
|||||||
|
|
||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
|
|
||||||
|
// Load Tiptap from CDN
|
||||||
|
this.tiptapBundle = await DeesServiceLibLoader.getInstance().loadTiptap();
|
||||||
|
|
||||||
this.editorElement = this.shadowRoot.querySelector('.editor-content');
|
this.editorElement = this.shadowRoot.querySelector('.editor-content');
|
||||||
this.linkInputElement = this.shadowRoot.querySelector('.link-input input');
|
this.linkInputElement = this.shadowRoot.querySelector('.link-input input');
|
||||||
this.initializeEditor();
|
this.initializeEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeEditor(): void {
|
private initializeEditor(): void {
|
||||||
if (this.disabled) return;
|
if (this.disabled || !this.tiptapBundle) return;
|
||||||
|
|
||||||
|
const { Editor, StarterKit, Underline, TextAlign, Link, Typography } = this.tiptapBundle;
|
||||||
|
|
||||||
this.editor = new Editor({
|
this.editor = new Editor({
|
||||||
element: this.editorElement,
|
element: this.editorElement,
|
||||||
@@ -249,7 +252,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
|||||||
levels: [1, 2, 3],
|
levels: [1, 2, 3],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
Underline,
|
Underline.configure({}),
|
||||||
TextAlign.configure({
|
TextAlign.configure({
|
||||||
types: ['heading', 'paragraph'],
|
types: ['heading', 'paragraph'],
|
||||||
}),
|
}),
|
||||||
@@ -259,7 +262,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
|||||||
class: 'editor-link',
|
class: 'editor-link',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
Typography,
|
Typography.configure({}),
|
||||||
],
|
],
|
||||||
content: this.value || (this.placeholder ? `<p>${this.placeholder}</p>` : ''),
|
content: this.value || (this.placeholder ? `<p>${this.placeholder}</p>` : ''),
|
||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor }) => {
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
|
|||||||
import type { IBlock } from '../../wysiwyg.types.js';
|
import type { IBlock } from '../../wysiwyg.types.js';
|
||||||
import { cssManager } from '@design.estate/dees-element';
|
import { cssManager } from '@design.estate/dees-element';
|
||||||
import { WysiwygSelection } from '../../wysiwyg.selection.js';
|
import { WysiwygSelection } from '../../wysiwyg.selection.js';
|
||||||
import hlight from 'highlight.js';
|
import type { HLJSApi } from 'highlight.js';
|
||||||
import { cssGeistFontFamily, cssMonoFontFamily } from '../../../../00fonts.js';
|
import { cssGeistFontFamily, cssMonoFontFamily } from '../../../../00fonts.js';
|
||||||
import { PROGRAMMING_LANGUAGES } from '../../wysiwyg.constants.js';
|
import { PROGRAMMING_LANGUAGES } from '../../wysiwyg.constants.js';
|
||||||
|
import { DeesServiceLibLoader } from '../../../../../services/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CodeBlockHandler with improved architecture
|
* CodeBlockHandler with improved architecture
|
||||||
@@ -18,8 +19,9 @@ import { PROGRAMMING_LANGUAGES } from '../../wysiwyg.constants.js';
|
|||||||
*/
|
*/
|
||||||
export class CodeBlockHandler extends BaseBlockHandler {
|
export class CodeBlockHandler extends BaseBlockHandler {
|
||||||
type = 'code';
|
type = 'code';
|
||||||
|
|
||||||
private highlightTimer: any = null;
|
private highlightTimer: any = null;
|
||||||
|
private highlightJs: HLJSApi | null = null;
|
||||||
|
|
||||||
render(block: IBlock, isSelected: boolean): string {
|
render(block: IBlock, isSelected: boolean): string {
|
||||||
const language = block.metadata?.language || 'typescript';
|
const language = block.metadata?.language || 'typescript';
|
||||||
@@ -306,28 +308,33 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
|||||||
return linesBeforeCursor.length - 1; // 0-indexed
|
return linesBeforeCursor.length - 1; // 0-indexed
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyHighlighting(element: HTMLElement, block: IBlock): void {
|
private async applyHighlighting(element: HTMLElement, block: IBlock): Promise<void> {
|
||||||
const editor = element.querySelector('.code-editor') as HTMLElement;
|
const editor = element.querySelector('.code-editor') as HTMLElement;
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
|
|
||||||
|
// Load highlight.js from CDN if not already loaded
|
||||||
|
if (!this.highlightJs) {
|
||||||
|
this.highlightJs = await DeesServiceLibLoader.getInstance().loadHighlightJs();
|
||||||
|
}
|
||||||
|
|
||||||
// Store cursor position
|
// Store cursor position
|
||||||
const cursorPos = this.getCursorPosition(element);
|
const cursorPos = this.getCursorPosition(element);
|
||||||
|
|
||||||
// Get plain text content
|
// Get plain text content
|
||||||
const content = editor.textContent || '';
|
const content = editor.textContent || '';
|
||||||
const language = block.metadata?.language || 'typescript';
|
const language = block.metadata?.language || 'typescript';
|
||||||
|
|
||||||
// Apply highlighting
|
// Apply highlighting
|
||||||
try {
|
try {
|
||||||
const result = hlight.highlight(content, {
|
const result = this.highlightJs.highlight(content, {
|
||||||
language: language,
|
language: language,
|
||||||
ignoreIllegals: true
|
ignoreIllegals: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only update if we have valid highlighted content
|
// Only update if we have valid highlighted content
|
||||||
if (result.value) {
|
if (result.value) {
|
||||||
editor.innerHTML = result.value;
|
editor.innerHTML = result.value;
|
||||||
|
|
||||||
// Restore cursor position if editor is focused
|
// Restore cursor position if editor is focused
|
||||||
if (document.activeElement === editor && cursorPos !== null) {
|
if (document.activeElement === editor && cursorPos !== null) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import {
|
|||||||
css,
|
css,
|
||||||
cssManager,
|
cssManager,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import { Terminal } from 'xterm';
|
import type { Terminal } from 'xterm';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import type { FitAddon } from 'xterm-addon-fit';
|
||||||
import { themeDefaultStyles } from '../../00theme.js';
|
import { themeDefaultStyles } from '../../00theme.js';
|
||||||
|
import { DeesServiceLibLoader } from '../../../services/index.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -305,8 +306,15 @@ export class DeesWorkspaceTerminalPreview extends DeesElement {
|
|||||||
const domtoolsInstance = await this.domtoolsPromise;
|
const domtoolsInstance = await this.domtoolsPromise;
|
||||||
const isBright = domtoolsInstance.themeManager.goBrightBoolean;
|
const isBright = domtoolsInstance.themeManager.goBrightBoolean;
|
||||||
|
|
||||||
// Create xterm terminal in read-only mode
|
// Load xterm from CDN
|
||||||
this.terminal = new Terminal({
|
const libLoader = DeesServiceLibLoader.getInstance();
|
||||||
|
const [xtermBundle, fitAddonBundle] = await Promise.all([
|
||||||
|
libLoader.loadXterm(),
|
||||||
|
libLoader.loadXtermFitAddon(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create xterm terminal in read-only mode using CDN-loaded module
|
||||||
|
this.terminal = new xtermBundle.Terminal({
|
||||||
convertEol: true,
|
convertEol: true,
|
||||||
cursorBlink: false,
|
cursorBlink: false,
|
||||||
disableStdin: true,
|
disableStdin: true,
|
||||||
@@ -323,7 +331,7 @@ export class DeesWorkspaceTerminalPreview extends DeesElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.fitAddon = new FitAddon();
|
this.fitAddon = new fitAddonBundle.FitAddon();
|
||||||
this.terminal.loadAddon(this.fitAddon);
|
this.terminal.loadAddon(this.fitAddon);
|
||||||
this.terminal.open(container);
|
this.terminal.open(container);
|
||||||
this.fitAddon.fit();
|
this.fitAddon.fit();
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import {
|
|||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
|
|
||||||
import { Terminal } from 'xterm';
|
import type { Terminal } from 'xterm';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
|
||||||
import { themeDefaultStyles } from '../../00theme.js';
|
import { themeDefaultStyles } from '../../00theme.js';
|
||||||
import type { IExecutionEnvironment } from '../../00group-runtime/index.js';
|
import type { IExecutionEnvironment } from '../../00group-runtime/index.js';
|
||||||
import { WebContainerEnvironment } from '../../00group-runtime/index.js';
|
import { WebContainerEnvironment } from '../../00group-runtime/index.js';
|
||||||
@@ -24,6 +23,7 @@ import type {
|
|||||||
ICreateTerminalTabOptions,
|
ICreateTerminalTabOptions,
|
||||||
TTerminalTabType,
|
TTerminalTabType,
|
||||||
} from './interfaces.js';
|
} from './interfaces.js';
|
||||||
|
import { DeesServiceLibLoader } from '../../../services/index.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -495,6 +495,16 @@ export class DeesWorkspaceTerminal extends DeesElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Load xterm from CDN
|
||||||
|
const libLoader = DeesServiceLibLoader.getInstance();
|
||||||
|
const [xtermBundle, fitAddonBundle] = await Promise.all([
|
||||||
|
libLoader.loadXterm(),
|
||||||
|
libLoader.loadXtermFitAddon(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Initialize tab manager with loaded modules
|
||||||
|
this.tabManager.setXtermModules(xtermBundle, fitAddonBundle);
|
||||||
|
|
||||||
// Create default shell tab
|
// Create default shell tab
|
||||||
await this.createShellTab();
|
await this.createShellTab();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Terminal } from 'xterm';
|
import type { Terminal, ITerminalOptions } from 'xterm';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import type { FitAddon } from 'xterm-addon-fit';
|
||||||
import type { ITerminalTab, ICreateTerminalTabOptions, TTerminalTabType } from './interfaces.js';
|
import type { ITerminalTab, ICreateTerminalTabOptions, TTerminalTabType } from './interfaces.js';
|
||||||
|
import type { IXtermBundle, IXtermFitAddonBundle } from '../../../services/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages terminal tabs lifecycle and state
|
* Manages terminal tabs lifecycle and state
|
||||||
@@ -8,6 +9,17 @@ import type { ITerminalTab, ICreateTerminalTabOptions, TTerminalTabType } from '
|
|||||||
export class TerminalTabManager {
|
export class TerminalTabManager {
|
||||||
private tabs: Map<string, ITerminalTab> = new Map();
|
private tabs: Map<string, ITerminalTab> = new Map();
|
||||||
private tabCounter: number = 0;
|
private tabCounter: number = 0;
|
||||||
|
private xtermBundle: IXtermBundle | null = null;
|
||||||
|
private xtermFitAddonBundle: IXtermFitAddonBundle | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the manager with loaded xterm modules.
|
||||||
|
* Must be called before creating tabs.
|
||||||
|
*/
|
||||||
|
public setXtermModules(xtermBundle: IXtermBundle, fitAddonBundle: IXtermFitAddonBundle): void {
|
||||||
|
this.xtermBundle = xtermBundle;
|
||||||
|
this.xtermFitAddonBundle = fitAddonBundle;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate unique tab ID
|
* Generate unique tab ID
|
||||||
@@ -96,11 +108,15 @@ export class TerminalTabManager {
|
|||||||
* Create a new tab instance
|
* Create a new tab instance
|
||||||
*/
|
*/
|
||||||
createTab(options: ICreateTerminalTabOptions, isBright: boolean): ITerminalTab {
|
createTab(options: ICreateTerminalTabOptions, isBright: boolean): ITerminalTab {
|
||||||
|
if (!this.xtermBundle || !this.xtermFitAddonBundle) {
|
||||||
|
throw new Error('TerminalTabManager: xterm modules not initialized. Call setXtermModules() first.');
|
||||||
|
}
|
||||||
|
|
||||||
const id = this.generateTabId();
|
const id = this.generateTabId();
|
||||||
const type = options.type;
|
const type = options.type;
|
||||||
|
|
||||||
// Create xterm.js Terminal instance
|
// Create xterm.js Terminal instance using CDN-loaded module
|
||||||
const terminal = new Terminal({
|
const terminal = new this.xtermBundle.Terminal({
|
||||||
convertEol: true,
|
convertEol: true,
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
theme: this.getTerminalTheme(isBright),
|
theme: this.getTerminalTheme(isBright),
|
||||||
@@ -109,8 +125,8 @@ export class TerminalTabManager {
|
|||||||
lineHeight: 1.2,
|
lineHeight: 1.2,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create FitAddon
|
// Create FitAddon using CDN-loaded module
|
||||||
const fitAddon = new FitAddon();
|
const fitAddon = new this.xtermFitAddonBundle.FitAddon();
|
||||||
terminal.loadAddon(fitAddon);
|
terminal.loadAddon(fitAddon);
|
||||||
|
|
||||||
const tab: ITerminalTab = {
|
const tab: ITerminalTab = {
|
||||||
|
|||||||
285
ts_web/services/DeesServiceLibLoader.ts
Normal file
285
ts_web/services/DeesServiceLibLoader.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import { CDN_BASE, CDN_VERSIONS } from './versions.js';
|
||||||
|
|
||||||
|
// Type imports (no runtime overhead)
|
||||||
|
import type { Terminal, ITerminalOptions } from 'xterm';
|
||||||
|
import type { FitAddon } from 'xterm-addon-fit';
|
||||||
|
import type { HLJSApi } from 'highlight.js';
|
||||||
|
import type ApexChartsType from 'apexcharts';
|
||||||
|
import type { Editor, EditorOptions } from '@tiptap/core';
|
||||||
|
import type { StarterKitOptions } from '@tiptap/starter-kit';
|
||||||
|
import type { UnderlineOptions } from '@tiptap/extension-underline';
|
||||||
|
import type { TextAlignOptions } from '@tiptap/extension-text-align';
|
||||||
|
import type { LinkOptions } from '@tiptap/extension-link';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle type for xterm and its addons
|
||||||
|
*/
|
||||||
|
export interface IXtermBundle {
|
||||||
|
Terminal: typeof Terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle type for xterm-addon-fit
|
||||||
|
*/
|
||||||
|
export interface IXtermFitAddonBundle {
|
||||||
|
FitAddon: typeof FitAddon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle type for Tiptap editor and extensions
|
||||||
|
*/
|
||||||
|
export interface ITiptapBundle {
|
||||||
|
Editor: typeof Editor;
|
||||||
|
StarterKit: { configure: (options?: Partial<StarterKitOptions>) => any };
|
||||||
|
Underline: { configure: (options?: Partial<UnderlineOptions>) => any };
|
||||||
|
TextAlign: { configure: (options?: Partial<TextAlignOptions>) => any };
|
||||||
|
Link: { configure: (options?: Partial<LinkOptions>) => any };
|
||||||
|
Typography: { configure: (options?: any) => any };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton service for lazy-loading heavy libraries from CDN.
|
||||||
|
*
|
||||||
|
* This reduces initial bundle size by loading libraries only when needed.
|
||||||
|
* Libraries are cached after first load to avoid duplicate fetches.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const libLoader = DeesServiceLibLoader.getInstance();
|
||||||
|
* const xterm = await libLoader.loadXterm();
|
||||||
|
* const terminal = new xterm.Terminal({ ... });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class DeesServiceLibLoader {
|
||||||
|
private static instance: DeesServiceLibLoader;
|
||||||
|
|
||||||
|
// Cached library references
|
||||||
|
private xtermLib: IXtermBundle | null = null;
|
||||||
|
private xtermFitAddonLib: IXtermFitAddonBundle | null = null;
|
||||||
|
private highlightJsLib: HLJSApi | null = null;
|
||||||
|
private apexChartsLib: typeof ApexChartsType | null = null;
|
||||||
|
private tiptapLib: ITiptapBundle | null = null;
|
||||||
|
|
||||||
|
// Loading promises to prevent duplicate concurrent loads
|
||||||
|
private xtermLoadingPromise: Promise<IXtermBundle> | null = null;
|
||||||
|
private xtermFitAddonLoadingPromise: Promise<IXtermFitAddonBundle> | null = null;
|
||||||
|
private highlightJsLoadingPromise: Promise<HLJSApi> | null = null;
|
||||||
|
private apexChartsLoadingPromise: Promise<typeof ApexChartsType> | null = null;
|
||||||
|
private tiptapLoadingPromise: Promise<ITiptapBundle> | null = null;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the singleton instance of DeesServiceLibLoader
|
||||||
|
*/
|
||||||
|
public static getInstance(): DeesServiceLibLoader {
|
||||||
|
if (!DeesServiceLibLoader.instance) {
|
||||||
|
DeesServiceLibLoader.instance = new DeesServiceLibLoader();
|
||||||
|
}
|
||||||
|
return DeesServiceLibLoader.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load xterm terminal emulator from CDN
|
||||||
|
* @returns Promise resolving to xterm module with Terminal class
|
||||||
|
*/
|
||||||
|
public async loadXterm(): Promise<IXtermBundle> {
|
||||||
|
if (this.xtermLib) {
|
||||||
|
return this.xtermLib;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.xtermLoadingPromise) {
|
||||||
|
return this.xtermLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.xtermLoadingPromise = (async () => {
|
||||||
|
const url = `${CDN_BASE}/xterm@${CDN_VERSIONS.xterm}/+esm`;
|
||||||
|
const module = await import(/* @vite-ignore */ url);
|
||||||
|
|
||||||
|
// Also load and inject xterm CSS
|
||||||
|
await this.injectXtermStyles();
|
||||||
|
|
||||||
|
this.xtermLib = {
|
||||||
|
Terminal: module.Terminal,
|
||||||
|
};
|
||||||
|
return this.xtermLib;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return this.xtermLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load xterm-addon-fit from CDN
|
||||||
|
* @returns Promise resolving to FitAddon class
|
||||||
|
*/
|
||||||
|
public async loadXtermFitAddon(): Promise<IXtermFitAddonBundle> {
|
||||||
|
if (this.xtermFitAddonLib) {
|
||||||
|
return this.xtermFitAddonLib;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.xtermFitAddonLoadingPromise) {
|
||||||
|
return this.xtermFitAddonLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.xtermFitAddonLoadingPromise = (async () => {
|
||||||
|
const url = `${CDN_BASE}/xterm-addon-fit@${CDN_VERSIONS.xtermAddonFit}/+esm`;
|
||||||
|
const module = await import(/* @vite-ignore */ url);
|
||||||
|
|
||||||
|
this.xtermFitAddonLib = {
|
||||||
|
FitAddon: module.FitAddon,
|
||||||
|
};
|
||||||
|
return this.xtermFitAddonLib;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return this.xtermFitAddonLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject xterm CSS styles into the document head
|
||||||
|
*/
|
||||||
|
private async injectXtermStyles(): Promise<void> {
|
||||||
|
const styleId = 'xterm-cdn-styles';
|
||||||
|
if (document.getElementById(styleId)) {
|
||||||
|
return; // Already injected
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssUrl = `${CDN_BASE}/xterm@${CDN_VERSIONS.xterm}/css/xterm.css`;
|
||||||
|
const response = await fetch(cssUrl);
|
||||||
|
const cssText = await response.text();
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = styleId;
|
||||||
|
style.textContent = cssText;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load highlight.js syntax highlighter from CDN
|
||||||
|
* @returns Promise resolving to highlight.js API
|
||||||
|
*/
|
||||||
|
public async loadHighlightJs(): Promise<HLJSApi> {
|
||||||
|
if (this.highlightJsLib) {
|
||||||
|
return this.highlightJsLib;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.highlightJsLoadingPromise) {
|
||||||
|
return this.highlightJsLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.highlightJsLoadingPromise = (async () => {
|
||||||
|
const url = `${CDN_BASE}/highlight.js@${CDN_VERSIONS.highlightJs}/+esm`;
|
||||||
|
const module = await import(/* @vite-ignore */ url);
|
||||||
|
|
||||||
|
this.highlightJsLib = module.default;
|
||||||
|
return this.highlightJsLib;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return this.highlightJsLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load ApexCharts charting library from CDN
|
||||||
|
* @returns Promise resolving to ApexCharts constructor
|
||||||
|
*/
|
||||||
|
public async loadApexCharts(): Promise<typeof ApexChartsType> {
|
||||||
|
if (this.apexChartsLib) {
|
||||||
|
return this.apexChartsLib;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.apexChartsLoadingPromise) {
|
||||||
|
return this.apexChartsLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.apexChartsLoadingPromise = (async () => {
|
||||||
|
const url = `${CDN_BASE}/apexcharts@${CDN_VERSIONS.apexcharts}/+esm`;
|
||||||
|
const module = await import(/* @vite-ignore */ url);
|
||||||
|
|
||||||
|
this.apexChartsLib = module.default;
|
||||||
|
return this.apexChartsLib;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return this.apexChartsLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Tiptap rich text editor and extensions from CDN
|
||||||
|
* @returns Promise resolving to Tiptap bundle with Editor and extensions
|
||||||
|
*/
|
||||||
|
public async loadTiptap(): Promise<ITiptapBundle> {
|
||||||
|
if (this.tiptapLib) {
|
||||||
|
return this.tiptapLib;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.tiptapLoadingPromise) {
|
||||||
|
return this.tiptapLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tiptapLoadingPromise = (async () => {
|
||||||
|
const version = CDN_VERSIONS.tiptap;
|
||||||
|
|
||||||
|
// Load all Tiptap modules in parallel
|
||||||
|
const [
|
||||||
|
coreModule,
|
||||||
|
starterKitModule,
|
||||||
|
underlineModule,
|
||||||
|
textAlignModule,
|
||||||
|
linkModule,
|
||||||
|
typographyModule,
|
||||||
|
] = await Promise.all([
|
||||||
|
import(/* @vite-ignore */ `${CDN_BASE}/@tiptap/core@${version}/+esm`),
|
||||||
|
import(/* @vite-ignore */ `${CDN_BASE}/@tiptap/starter-kit@${version}/+esm`),
|
||||||
|
import(/* @vite-ignore */ `${CDN_BASE}/@tiptap/extension-underline@${version}/+esm`),
|
||||||
|
import(/* @vite-ignore */ `${CDN_BASE}/@tiptap/extension-text-align@${version}/+esm`),
|
||||||
|
import(/* @vite-ignore */ `${CDN_BASE}/@tiptap/extension-link@${version}/+esm`),
|
||||||
|
import(/* @vite-ignore */ `${CDN_BASE}/@tiptap/extension-typography@${version}/+esm`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.tiptapLib = {
|
||||||
|
Editor: coreModule.Editor,
|
||||||
|
StarterKit: starterKitModule.default || starterKitModule.StarterKit,
|
||||||
|
Underline: underlineModule.default || underlineModule.Underline,
|
||||||
|
TextAlign: textAlignModule.default || textAlignModule.TextAlign,
|
||||||
|
Link: linkModule.default || linkModule.Link,
|
||||||
|
Typography: typographyModule.default || typographyModule.Typography,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.tiptapLib;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return this.tiptapLoadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preload multiple libraries in parallel
|
||||||
|
* Useful for warming the cache before components are rendered
|
||||||
|
*/
|
||||||
|
public async preloadAll(): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadXterm(),
|
||||||
|
this.loadXtermFitAddon(),
|
||||||
|
this.loadHighlightJs(),
|
||||||
|
this.loadApexCharts(),
|
||||||
|
this.loadTiptap(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific library is already loaded
|
||||||
|
*/
|
||||||
|
public isLoaded(library: 'xterm' | 'xtermFitAddon' | 'highlightJs' | 'apexCharts' | 'tiptap'): boolean {
|
||||||
|
switch (library) {
|
||||||
|
case 'xterm':
|
||||||
|
return this.xtermLib !== null;
|
||||||
|
case 'xtermFitAddon':
|
||||||
|
return this.xtermFitAddonLib !== null;
|
||||||
|
case 'highlightJs':
|
||||||
|
return this.highlightJsLib !== null;
|
||||||
|
case 'apexCharts':
|
||||||
|
return this.apexChartsLib !== null;
|
||||||
|
case 'tiptap':
|
||||||
|
return this.tiptapLib !== null;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
ts_web/services/index.ts
Normal file
3
ts_web/services/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { DeesServiceLibLoader } from './DeesServiceLibLoader.js';
|
||||||
|
export type { IXtermBundle, IXtermFitAddonBundle, ITiptapBundle } from './DeesServiceLibLoader.js';
|
||||||
|
export { CDN_BASE, CDN_VERSIONS } from './versions.js';
|
||||||
17
ts_web/services/versions.ts
Normal file
17
ts_web/services/versions.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* CDN versions for lazy-loaded libraries.
|
||||||
|
* Keep these in sync with package.json for type compatibility.
|
||||||
|
*/
|
||||||
|
export const CDN_VERSIONS = {
|
||||||
|
xterm: '5.3.0',
|
||||||
|
xtermAddonFit: '0.8.0',
|
||||||
|
highlightJs: '11.11.1',
|
||||||
|
apexcharts: '5.3.6',
|
||||||
|
tiptap: '2.23.0',
|
||||||
|
fontawesome: '7.1.0',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base CDN URL for jsdelivr ESM imports
|
||||||
|
*/
|
||||||
|
export const CDN_BASE = 'https://cdn.jsdelivr.net/npm';
|
||||||
Reference in New Issue
Block a user