Prerequisites
- Strong understanding of ES6 modules and module systems ๐
- Experience with TypeScript compilation and bundling โก
- Knowledge of dynamic imports and code splitting ๐ป
What you'll learn
- Master SystemJS for dynamic module loading and dependency management ๐ฏ
- Build flexible architectures with runtime module resolution ๐๏ธ
- Implement advanced patterns like module federation and micro-frontends ๐
- Create production-ready SystemJS applications with TypeScript โจ
๐ฏ Introduction
Welcome to the dynamic universe of SystemJS! โก If traditional module systems are like pre-planned train routes, then SystemJS is your personal teleportation device - it can load any module from anywhere, at any time, with infinite flexibility and power!
SystemJS is a universal dynamic module loader that can load ES6 modules, CommonJS, AMD, and global scripts on the fly. Itโs particularly powerful for creating micro-frontends, plugin architectures, and applications that need to load modules conditionally at runtime. With TypeScript, SystemJS becomes even more powerful, providing type safety while maintaining incredible flexibility.
By the end of this tutorial, youโll be a master of dynamic module loading with SystemJS and TypeScript, capable of building sophisticated applications that adapt and evolve at runtime. Letโs dive into the world of dynamic loading! ๐
๐ Understanding SystemJS
๐ค What Is SystemJS?
SystemJS is a universal module loader that brings ES6 module loading to browsers and can load virtually any module format. It provides a polyfill for the ES Module Loader spec and supports features like dynamic imports, conditional loading, and runtime dependency resolution.
// ๐ Basic SystemJS concepts and usage
// SystemJS can load modules dynamically at runtime
// ๐ฏ Basic dynamic import with SystemJS
async function loadMathModule() {
try {
console.log('๐ฆ Loading math module dynamically...');
// ๐ Dynamic import using SystemJS
const mathModule = await System.import('./modules/math-utils.js');
console.log('โ
Math module loaded successfully!');
console.log('Available functions:', Object.keys(mathModule));
// ๐งฎ Use the dynamically loaded module
const result = mathModule.add(10, 5);
console.log(`โ 10 + 5 = ${result}`);
return mathModule;
} catch (error) {
console.error('โ Failed to load math module:', error);
throw error;
}
}
// ๐ฏ Conditional module loading based on environment
async function loadEnvironmentSpecificModule() {
const isProduction = process.env.NODE_ENV === 'production';
const isDevelopment = process.env.NODE_ENV === 'development';
console.log(`๐ Environment: ${process.env.NODE_ENV}`);
if (isProduction) {
console.log('๐ Loading production modules...');
const [analytics, monitoring] = await Promise.all([
System.import('./modules/analytics.js'),
System.import('./modules/monitoring.js')
]);
return { analytics, monitoring };
} else if (isDevelopment) {
console.log('๐ ๏ธ Loading development modules...');
const [devTools, debugger] = await Promise.all([
System.import('./modules/dev-tools.js'),
System.import('./modules/debugger.js')
]);
return { devTools, debugger };
} else {
console.log('๐ง Loading default modules...');
const baseModule = await System.import('./modules/base.js');
return { base: baseModule };
}
}
// ๐ฎ Feature-based dynamic loading
async function loadFeatureModules(features: string[]) {
console.log(`๐ฏ Loading feature modules: ${features.join(', ')}`);
const modulePromises = features.map(async (feature) => {
try {
const module = await System.import(`./features/${feature}.js`);
console.log(`โ
Feature "${feature}" loaded successfully`);
return { feature, module, loaded: true };
} catch (error) {
console.error(`โ Failed to load feature "${feature}":`, error);
return { feature, module: null, loaded: false, error };
}
});
const results = await Promise.allSettled(modulePromises);
return results.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value;
} else {
return {
feature: features[index],
module: null,
loaded: false,
error: result.reason
};
}
});
}
// ๐ Module hot-reloading with SystemJS
class ModuleHotReloader {
private loadedModules = new Map<string, any>();
private moduleVersions = new Map<string, number>();
private reloadCallbacks = new Map<string, Set<(module: any) => void>>();
async loadModule(specifier: string, version?: number): Promise<any> {
const currentVersion = this.moduleVersions.get(specifier) || 0;
const targetVersion = version || currentVersion;
console.log(`๐ Loading module: ${specifier} (version ${targetVersion})`);
try {
// ๐งน Clear module from SystemJS cache if reloading
if (this.loadedModules.has(specifier) && targetVersion > currentVersion) {
console.log(`๐๏ธ Clearing cached version of: ${specifier}`);
System.delete(specifier);
}
// ๐ฆ Load the module
const module = await System.import(`${specifier}?v=${targetVersion}`);
// ๐พ Cache the module and version
this.loadedModules.set(specifier, module);
this.moduleVersions.set(specifier, targetVersion);
// ๐ Notify reload callbacks
const callbacks = this.reloadCallbacks.get(specifier);
if (callbacks) {
callbacks.forEach(callback => {
try {
callback(module);
console.log(`๐ Reload callback executed for: ${specifier}`);
} catch (error) {
console.error(`โ Reload callback failed for ${specifier}:`, error);
}
});
}
console.log(`โ
Module loaded successfully: ${specifier}`);
return module;
} catch (error) {
console.error(`โ Failed to load module ${specifier}:`, error);
throw error;
}
}
onModuleReload(specifier: string, callback: (module: any) => void): void {
if (!this.reloadCallbacks.has(specifier)) {
this.reloadCallbacks.set(specifier, new Set());
}
this.reloadCallbacks.get(specifier)!.add(callback);
console.log(`๐ง Reload callback registered for: ${specifier}`);
}
async reloadModule(specifier: string): Promise<any> {
const currentVersion = this.moduleVersions.get(specifier) || 0;
const newVersion = currentVersion + 1;
console.log(`๐ Hot reloading module: ${specifier}`);
return this.loadModule(specifier, newVersion);
}
getLoadedModules(): string[] {
return Array.from(this.loadedModules.keys());
}
isModuleLoaded(specifier: string): boolean {
return this.loadedModules.has(specifier);
}
getModuleVersion(specifier: string): number {
return this.moduleVersions.get(specifier) || 0;
}
}
// ๐ฎ Usage examples
async function systemJSExamples() {
// ๐งฎ Load math module
const mathModule = await loadMathModule();
// ๐ Load environment-specific modules
const envModules = await loadEnvironmentSpecificModule();
// ๐ฏ Load feature modules
const featureResults = await loadFeatureModules(['user-management', 'analytics', 'notifications']);
// ๐ Set up hot reloading
const hotReloader = new ModuleHotReloader();
// Load a module with hot reloading
const userModule = await hotReloader.loadModule('./modules/user-service.js');
// Register reload callback
hotReloader.onModuleReload('./modules/user-service.js', (newModule) => {
console.log('๐ User service module has been reloaded!');
// Update application state with new module
});
console.log('๐ SystemJS examples completed!');
}
๐ก SystemJS Key Features
- โก Dynamic Loading: Load modules at runtime based on conditions
- ๐ Hot Reloading: Replace modules without page refresh
- ๐ Universal Format Support: ES6, CommonJS, AMD, and globals
- ๐ฆ Code Splitting: Automatic chunk loading and optimization
- ๐ฏ Conditional Loading: Load different modules based on environment
- ๐ง Plugin System: Extensible with custom loaders and transforms
๐ ๏ธ Setting Up SystemJS with TypeScript
๐ฆ Installation and Configuration
Letโs set up a complete SystemJS development environment:
// ๐ Package.json for SystemJS development
{
"name": "typescript-systemjs-app",
"version": "1.0.0",
"description": "Advanced TypeScript application with SystemJS",
"main": "dist/index.js",
"scripts": {
"build": "npm run clean && npm run compile && npm run bundle",
"compile": "tsc",
"bundle": "systemjs-builder src/index.ts dist/bundle.js --minify",
"dev": "npm run compile && concurrently \"npm run serve\" \"npm run watch\"",
"watch": "tsc --watch",
"serve": "http-server . -p 8080 -o",
"clean": "rimraf dist",
"lint": "eslint src/**/*.ts",
"test": "jest",
"build:production": "NODE_ENV=production npm run build"
},
"dependencies": {
"systemjs": "^6.14.2"
},
"devDependencies": {
"typescript": "^5.3.0",
"systemjs-builder": "^0.21.0",
"http-server": "^14.1.1",
"concurrently": "^8.2.2",
"rimraf": "^5.0.5",
"@types/systemjs": "^6.13.0",
"eslint": "^8.54.0",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"jest": "^29.7.0",
"@types/jest": "^29.5.8",
"ts-jest": "^29.1.1"
}
}
// ๐ง TypeScript configuration for SystemJS
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020", // Modern JavaScript for better performance
"module": "System", // ๐ฏ SystemJS module format
"lib": ["ES2020", "DOM"], // Include necessary libraries
"outDir": "./dist", // Output directory
"rootDir": "./src", // Source directory
"strict": true, // Enable strict type checking
"esModuleInterop": true, // Enable ES module interop
"allowSyntheticDefaultImports": true, // Allow synthetic imports
"skipLibCheck": true, // Skip library type checking
"forceConsistentCasingInFileNames": true, // Enforce consistent casing
"moduleResolution": "node", // Node-style module resolution
"declaration": true, // Generate .d.ts files
"declarationDir": "./dist/types", // Type declarations directory
"sourceMap": true, // Generate source maps
"removeComments": false, // Keep comments for debugging
"experimentalDecorators": true, // Enable decorators
"emitDecoratorMetadata": true, // Emit decorator metadata
"resolveJsonModule": true, // Enable JSON imports
"isolatedModules": true, // Ensure isolated modules
"noEmitOnError": true, // Don't emit on errors
"strictNullChecks": true, // Strict null checking
"noImplicitAny": true, // No implicit any types
"noImplicitReturns": true, // No implicit returns
"noUnusedLocals": true, // Flag unused locals
"noUnusedParameters": true, // Flag unused parameters
"exactOptionalPropertyTypes": true, // Exact optional properties
// ๐ฏ SystemJS-specific options
"baseUrl": "./src", // Base URL for module resolution
"paths": { // Path mapping
"@/*": ["*"],
"@components/*": ["components/*"],
"@services/*": ["services/*"],
"@utils/*": ["utils/*"],
"@types/*": ["types/*"],
"@features/*": ["features/*"]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.spec.ts",
"**/*.test.ts"
]
}
// ๐ SystemJS configuration file
// systemjs.config.js
System.config({
// ๐ Base URL for module loading
baseURL: '/dist',
// ๐บ๏ธ Default extension for module imports
defaultJSExtensions: true,
// ๐ฏ Transpiler configuration
transpiler: 'typescript',
// ๐ฆ Package configurations
packages: {
'/dist': {
defaultExtension: 'js'
},
'/src': {
defaultExtension: 'ts'
}
},
// ๐บ๏ธ Path mappings
map: {
// ๐ง TypeScript transpiler
'typescript': 'https://unpkg.com/[email protected]/lib/typescript.js',
// ๐ฆ External libraries
'rxjs': 'https://unpkg.com/[email protected]/dist/bundles/rxjs.umd.min.js',
'lodash': 'https://unpkg.com/[email protected]/lodash.min.js',
'moment': 'https://unpkg.com/[email protected]/min/moment.min.js',
// ๐ฏ Application modules
'@components': '/dist/components',
'@services': '/dist/services',
'@utils': '/dist/utils',
'@types': '/dist/types',
'@features': '/dist/features'
},
// ๐ง Meta configuration for specific modules
meta: {
'typescript': {
exports: 'ts'
},
'*.ts': {
loader: 'typescript'
},
'*.json': {
loader: 'json'
}
},
// ๐ฆ Bundle configuration for production
bundles: {
'core-bundle.js': ['@components/*', '@services/*', '@utils/*'],
'features-bundle.js': ['@features/*'],
'vendor-bundle.js': ['rxjs', 'lodash', 'moment']
},
// ๐ง Build configuration
buildConfig: {
minify: true,
mangle: true,
sourceMaps: true
}
});
// ๐ Bootstrap the application
System.import('./main.js').catch(console.error);
๐๏ธ Project Structure for SystemJS
// ๐ Comprehensive project structure for SystemJS applications
src/
โโโ index.html // Main HTML file with SystemJS
โโโ main.ts // Application entry point
โโโ systemjs.config.js // SystemJS configuration
โโโ core/ // Core application modules
โ โโโ app.ts // Main application class
โ โโโ router.ts // Dynamic routing system
โ โโโ module-loader.ts // Advanced module loading utilities
โ โโโ plugin-system.ts // Plugin architecture
โโโ components/ // Dynamically loadable UI components
โ โโโ button/
โ โ โโโ button.ts
โ โ โโโ button.css
โ โ โโโ index.ts
โ โโโ modal/
โ โ โโโ modal.ts
โ โ โโโ modal.css
โ โ โโโ index.ts
โ โโโ form/
โ โโโ form.ts
โ โโโ form.css
โ โโโ index.ts
โโโ services/ // Business logic services
โ โโโ api-service.ts
โ โโโ auth-service.ts
โ โโโ state-manager.ts
โ โโโ notification-service.ts
โโโ features/ // Feature modules (micro-frontends)
โ โโโ user-management/
โ โ โโโ index.ts
โ โ โโโ user-list.ts
โ โ โโโ user-form.ts
โ โ โโโ user-service.ts
โ โโโ analytics/
โ โ โโโ index.ts
โ โ โโโ dashboard.ts
โ โ โโโ charts.ts
โ โ โโโ analytics-service.ts
โ โโโ notifications/
โ โโโ index.ts
โ โโโ notification-center.ts
โ โโโ notification-item.ts
โ โโโ notification-service.ts
โโโ plugins/ // Dynamic plugins
โ โโโ plugin-interface.ts
โ โโโ theme-plugin.ts
โ โโโ i18n-plugin.ts
โ โโโ analytics-plugin.ts
โโโ utils/ // Utility modules
โ โโโ dom-utils.ts
โ โโโ validation.ts
โ โโโ formatters.ts
โ โโโ async-utils.ts
โโโ types/ // Type definitions
โ โโโ api-types.ts
โ โโโ app-types.ts
โ โโโ component-types.ts
โ โโโ plugin-types.ts
โโโ assets/ // Static assets
โโโ styles/
โโโ images/
โโโ fonts/
// ๐ฏ dist/ (generated after compilation)
dist/
โโโ main.js
โโโ systemjs.config.js
โโโ core/
โโโ components/
โโโ services/
โโโ features/
โโโ plugins/
โโโ utils/
โโโ types/
โโโ bundles/ // Production bundles
โ โโโ core-bundle.js
โ โโโ features-bundle.js
โ โโโ vendor-bundle.js
โโโ assets/
๐๏ธ Advanced SystemJS Patterns
๐ฏ Dynamic Module Registry and Plugin System
Letโs create a sophisticated plugin system using SystemJS:
// ๐ Advanced plugin system with dynamic loading
// src/core/plugin-system.ts
export interface PluginManifest {
name: string;
version: string;
description: string;
author: string;
entry: string;
dependencies?: string[];
permissions?: string[];
config?: Record<string, any>;
}
export interface Plugin {
manifest: PluginManifest;
activate(context: PluginContext): Promise<void> | void;
deactivate?(): Promise<void> | void;
onUpdate?(oldVersion: string, newVersion: string): Promise<void> | void;
}
export interface PluginContext {
app: any;
services: Record<string, any>;
registerCommand(id: string, handler: Function): void;
registerComponent(name: string, component: any): void;
registerService(name: string, service: any): void;
getConfig(key: string): any;
setConfig(key: string, value: any): void;
emit(event: string, data?: any): void;
on(event: string, handler: Function): void;
}
// ๐ฏ Advanced plugin manager
export class PluginManager {
private plugins = new Map<string, Plugin>();
private manifests = new Map<string, PluginManifest>();
private loadedModules = new Map<string, any>();
private dependencyGraph = new Map<string, Set<string>>();
private eventEmitter = new EventTarget();
private config = new Map<string, any>();
constructor(private app: any, private services: Record<string, any>) {
console.log('๐ Plugin Manager initialized');
}
// ๐ Discover available plugins
async discoverPlugins(pluginDirectory: string = './plugins'): Promise<PluginManifest[]> {
console.log(`๐ Discovering plugins in: ${pluginDirectory}`);
try {
// Load plugin registry
const registry = await System.import(`${pluginDirectory}/registry.json`);
const plugins: PluginManifest[] = registry.plugins || [];
console.log(`๐ฆ Found ${plugins.length} plugins`);
// Validate and store manifests
for (const manifest of plugins) {
if (this.validateManifest(manifest)) {
this.manifests.set(manifest.name, manifest);
console.log(`โ
Plugin manifest validated: ${manifest.name}`);
} else {
console.warn(`โ ๏ธ Invalid plugin manifest: ${manifest.name}`);
}
}
return Array.from(this.manifests.values());
} catch (error) {
console.error('โ Failed to discover plugins:', error);
return [];
}
}
// ๐ Load plugin with dependency resolution
async loadPlugin(pluginName: string): Promise<boolean> {
console.log(`๐ Loading plugin: ${pluginName}`);
const manifest = this.manifests.get(pluginName);
if (!manifest) {
console.error(`โ Plugin manifest not found: ${pluginName}`);
return false;
}
// ๐ Check and load dependencies first
if (manifest.dependencies) {
console.log(`๐ Loading dependencies for ${pluginName}:`, manifest.dependencies);
for (const dependency of manifest.dependencies) {
if (!this.plugins.has(dependency)) {
const dependencyLoaded = await this.loadPlugin(dependency);
if (!dependencyLoaded) {
console.error(`โ Failed to load dependency: ${dependency}`);
return false;
}
}
}
}
try {
// ๐ฆ Load the plugin module
console.log(`๐ฆ Loading plugin module: ${manifest.entry}`);
const pluginModule = await System.import(manifest.entry);
// ๐ฏ Create plugin instance
const plugin: Plugin = new pluginModule.default(manifest);
// ๐ง Create plugin context
const context = this.createPluginContext(pluginName);
// ๐ Activate the plugin
await plugin.activate(context);
// ๐พ Store plugin
this.plugins.set(pluginName, plugin);
this.loadedModules.set(pluginName, pluginModule);
// ๐ Update dependency graph
if (manifest.dependencies) {
this.dependencyGraph.set(pluginName, new Set(manifest.dependencies));
}
// ๐ Emit plugin loaded event
this.emitEvent('plugin:loaded', { pluginName, manifest });
console.log(`โ
Plugin loaded successfully: ${pluginName}`);
return true;
} catch (error) {
console.error(`โ Failed to load plugin ${pluginName}:`, error);
return false;
}
}
// ๐๏ธ Unload plugin with dependent cleanup
async unloadPlugin(pluginName: string): Promise<boolean> {
console.log(`๐๏ธ Unloading plugin: ${pluginName}`);
const plugin = this.plugins.get(pluginName);
if (!plugin) {
console.warn(`โ ๏ธ Plugin not loaded: ${pluginName}`);
return false;
}
try {
// ๐ Check for dependent plugins
const dependents = this.getDependentPlugins(pluginName);
if (dependents.length > 0) {
console.log(`๐ Unloading dependent plugins: ${dependents.join(', ')}`);
for (const dependent of dependents) {
await this.unloadPlugin(dependent);
}
}
// ๐ Deactivate the plugin
if (plugin.deactivate) {
await plugin.deactivate();
}
// ๐งน Clean up
this.plugins.delete(pluginName);
this.loadedModules.delete(pluginName);
this.dependencyGraph.delete(pluginName);
// ๐๏ธ Remove from SystemJS cache
const manifest = this.manifests.get(pluginName);
if (manifest) {
System.delete(manifest.entry);
}
// ๐ Emit plugin unloaded event
this.emitEvent('plugin:unloaded', { pluginName });
console.log(`โ
Plugin unloaded successfully: ${pluginName}`);
return true;
} catch (error) {
console.error(`โ Failed to unload plugin ${pluginName}:`, error);
return false;
}
}
// ๐ Reload plugin (hot reload)
async reloadPlugin(pluginName: string): Promise<boolean> {
console.log(`๐ Reloading plugin: ${pluginName}`);
const manifest = this.manifests.get(pluginName);
if (!manifest) {
console.error(`โ Plugin manifest not found: ${pluginName}`);
return false;
}
// Store current version for update callback
const oldVersion = manifest.version;
// ๐๏ธ Unload current version
await this.unloadPlugin(pluginName);
// ๐ Clear from SystemJS cache with version parameter
const versionedEntry = `${manifest.entry}?v=${Date.now()}`;
System.delete(manifest.entry);
try {
// ๐ฆ Load new version
console.log(`๐ฆ Loading new version: ${versionedEntry}`);
const pluginModule = await System.import(versionedEntry);
// ๐ฏ Create new plugin instance
const plugin: Plugin = new pluginModule.default(manifest);
// ๐ง Create plugin context
const context = this.createPluginContext(pluginName);
// ๐ Activate the plugin
await plugin.activate(context);
// ๐ Call update callback if available
if (plugin.onUpdate) {
await plugin.onUpdate(oldVersion, manifest.version);
}
// ๐พ Store plugin
this.plugins.set(pluginName, plugin);
this.loadedModules.set(pluginName, pluginModule);
// ๐ Emit plugin reloaded event
this.emitEvent('plugin:reloaded', { pluginName, oldVersion, newVersion: manifest.version });
console.log(`๐ Plugin reloaded successfully: ${pluginName}`);
return true;
} catch (error) {
console.error(`โ Failed to reload plugin ${pluginName}:`, error);
return false;
}
}
// ๐ง Create plugin context
private createPluginContext(pluginName: string): PluginContext {
return {
app: this.app,
services: this.services,
registerCommand: (id: string, handler: Function) => {
console.log(`๐ Command registered: ${id} by ${pluginName}`);
this.app.commandRegistry.register(id, handler, pluginName);
},
registerComponent: (name: string, component: any) => {
console.log(`๐จ Component registered: ${name} by ${pluginName}`);
this.app.componentRegistry.register(name, component, pluginName);
},
registerService: (name: string, service: any) => {
console.log(`๐ง Service registered: ${name} by ${pluginName}`);
this.services[name] = service;
},
getConfig: (key: string) => {
const pluginConfig = this.config.get(`${pluginName}.${key}`);
return pluginConfig !== undefined ? pluginConfig : this.config.get(key);
},
setConfig: (key: string, value: any) => {
this.config.set(`${pluginName}.${key}`, value);
console.log(`โ๏ธ Config set: ${key} = ${value} by ${pluginName}`);
},
emit: (event: string, data?: any) => {
this.emitEvent(`plugin:${pluginName}:${event}`, data);
},
on: (event: string, handler: Function) => {
this.addEventListener(`plugin:${pluginName}:${event}`, handler);
}
};
}
// ๐ Get dependent plugins
private getDependentPlugins(pluginName: string): string[] {
const dependents: string[] = [];
for (const [name, dependencies] of this.dependencyGraph) {
if (dependencies.has(pluginName)) {
dependents.push(name);
}
}
return dependents;
}
// โ
Validate plugin manifest
private validateManifest(manifest: PluginManifest): boolean {
const required = ['name', 'version', 'entry'];
for (const field of required) {
if (!(field in manifest)) {
console.error(`โ Missing required field in manifest: ${field}`);
return false;
}
}
// Validate version format
const versionRegex = /^\d+\.\d+\.\d+$/;
if (!versionRegex.test(manifest.version)) {
console.error(`โ Invalid version format: ${manifest.version}`);
return false;
}
return true;
}
// ๐ Event management
private emitEvent(event: string, data?: any): void {
this.eventEmitter.dispatchEvent(new CustomEvent(event, { detail: data }));
}
private addEventListener(event: string, handler: Function): void {
this.eventEmitter.addEventListener(event, handler as EventListener);
}
// ๐ Plugin management utilities
getLoadedPlugins(): string[] {
return Array.from(this.plugins.keys());
}
getAvailablePlugins(): string[] {
return Array.from(this.manifests.keys());
}
isPluginLoaded(pluginName: string): boolean {
return this.plugins.has(pluginName);
}
getPluginManifest(pluginName: string): PluginManifest | undefined {
return this.manifests.get(pluginName);
}
getPluginInfo(): Array<{
name: string;
version: string;
loaded: boolean;
dependencies?: string[];
}> {
return Array.from(this.manifests.values()).map(manifest => ({
name: manifest.name,
version: manifest.version,
loaded: this.plugins.has(manifest.name),
dependencies: manifest.dependencies,
}));
}
// ๐ Bulk operations
async loadAllPlugins(): Promise<boolean[]> {
console.log('๐ Loading all available plugins...');
const results: boolean[] = [];
const sortedPlugins = this.topologicalSort();
for (const pluginName of sortedPlugins) {
const result = await this.loadPlugin(pluginName);
results.push(result);
}
return results;
}
async unloadAllPlugins(): Promise<boolean[]> {
console.log('๐ Unloading all plugins...');
const results: boolean[] = [];
const pluginNames = Array.from(this.plugins.keys()).reverse(); // Reverse order
for (const pluginName of pluginNames) {
const result = await this.unloadPlugin(pluginName);
results.push(result);
}
return results;
}
// ๐ Topological sort for dependency resolution
private topologicalSort(): string[] {
const sorted: string[] = [];
const visited = new Set<string>();
const visiting = new Set<string>();
const visit = (pluginName: string) => {
if (visiting.has(pluginName)) {
throw new Error(`Circular dependency detected: ${pluginName}`);
}
if (visited.has(pluginName)) {
return;
}
visiting.add(pluginName);
const manifest = this.manifests.get(pluginName);
if (manifest?.dependencies) {
for (const dependency of manifest.dependencies) {
visit(dependency);
}
}
visiting.delete(pluginName);
visited.add(pluginName);
sorted.push(pluginName);
};
for (const pluginName of this.manifests.keys()) {
if (!visited.has(pluginName)) {
visit(pluginName);
}
}
return sorted;
}
}
// ๐ฎ Example plugin implementation
// src/plugins/theme-plugin.ts
export default class ThemePlugin implements Plugin {
manifest: PluginManifest;
private themes = new Map<string, any>();
private currentTheme = 'default';
constructor(manifest: PluginManifest) {
this.manifest = manifest;
}
async activate(context: PluginContext): Promise<void> {
console.log('๐จ Activating Theme Plugin...');
// ๐ฆ Load available themes
await this.loadThemes();
// ๐ Register commands
context.registerCommand('theme:switch', (themeName: string) => {
this.switchTheme(themeName);
});
context.registerCommand('theme:list', () => {
return Array.from(this.themes.keys());
});
// ๐ง Register service
context.registerService('themeService', {
switchTheme: (name: string) => this.switchTheme(name),
getCurrentTheme: () => this.currentTheme,
getAvailableThemes: () => Array.from(this.themes.keys()),
});
// โ๏ธ Load saved theme preference
const savedTheme = context.getConfig('currentTheme');
if (savedTheme && this.themes.has(savedTheme)) {
this.switchTheme(savedTheme);
}
console.log('โ
Theme Plugin activated');
}
async deactivate(): Promise<void> {
console.log('๐ Deactivating Theme Plugin...');
// Cleanup logic here
}
private async loadThemes(): Promise<void> {
try {
const themeModules = await Promise.all([
System.import('./themes/light-theme.js'),
System.import('./themes/dark-theme.js'),
System.import('./themes/blue-theme.js'),
]);
themeModules.forEach((module, index) => {
const themeNames = ['light', 'dark', 'blue'];
this.themes.set(themeNames[index], module.default);
});
console.log(`๐จ Loaded ${this.themes.size} themes`);
} catch (error) {
console.error('โ Failed to load themes:', error);
}
}
private switchTheme(themeName: string): boolean {
if (!this.themes.has(themeName)) {
console.warn(`โ ๏ธ Theme not found: ${themeName}`);
return false;
}
const theme = this.themes.get(themeName);
// Apply theme to document
document.documentElement.className = `theme-${themeName}`;
// Apply CSS variables
if (theme.cssVariables) {
Object.entries(theme.cssVariables).forEach(([key, value]) => {
document.documentElement.style.setProperty(key, value as string);
});
}
this.currentTheme = themeName;
console.log(`๐จ Switched to theme: ${themeName}`);
return true;
}
}
๐ Micro-Frontend Architecture
Letโs implement a micro-frontend system using SystemJS:
// ๐ Micro-frontend architecture with SystemJS
// src/core/micro-frontend-manager.ts
export interface MicroFrontendConfig {
name: string;
entry: string;
route: string;
version?: string;
dependencies?: string[];
metadata?: Record<string, any>;
}
export interface MicroFrontend {
name: string;
mount(container: HTMLElement, props?: any): Promise<void> | void;
unmount?(container: HTMLElement): Promise<void> | void;
update?(props: any): Promise<void> | void;
getInfo?(): any;
}
// ๐ฏ Micro-frontend manager
export class MicroFrontendManager {
private frontends = new Map<string, MicroFrontend>();
private configs = new Map<string, MicroFrontendConfig>();
private mountedFrontends = new Map<string, HTMLElement>();
private routeMap = new Map<string, string>(); // route -> frontend name
constructor() {
console.log('๐ Micro-Frontend Manager initialized');
this.setupRouting();
}
// ๐ Register micro-frontend
registerMicroFrontend(config: MicroFrontendConfig): void {
console.log(`๐ Registering micro-frontend: ${config.name}`);
this.configs.set(config.name, config);
this.routeMap.set(config.route, config.name);
console.log(`โ
Micro-frontend registered: ${config.name} -> ${config.route}`);
}
// ๐ Load micro-frontend
async loadMicroFrontend(name: string): Promise<boolean> {
console.log(`๐ Loading micro-frontend: ${name}`);
const config = this.configs.get(name);
if (!config) {
console.error(`โ Micro-frontend config not found: ${name}`);
return false;
}
try {
// ๐ Load dependencies first
if (config.dependencies) {
console.log(`๐ Loading dependencies for ${name}:`, config.dependencies);
await Promise.all(
config.dependencies.map(dep => System.import(dep))
);
}
// ๐ฆ Load the micro-frontend module
const entryUrl = config.version
? `${config.entry}?v=${config.version}`
: config.entry;
console.log(`๐ฆ Loading micro-frontend module: ${entryUrl}`);
const module = await System.import(entryUrl);
// ๐ฏ Create micro-frontend instance
const microFrontend: MicroFrontend = module.default || module;
// โ
Validate micro-frontend interface
if (typeof microFrontend.mount !== 'function') {
throw new Error(`Micro-frontend ${name} must implement mount() method`);
}
this.frontends.set(name, microFrontend);
console.log(`โ
Micro-frontend loaded successfully: ${name}`);
return true;
} catch (error) {
console.error(`โ Failed to load micro-frontend ${name}:`, error);
return false;
}
}
// ๐๏ธ Mount micro-frontend
async mountMicroFrontend(
name: string,
container: HTMLElement,
props?: any
): Promise<boolean> {
console.log(`๐๏ธ Mounting micro-frontend: ${name}`);
let microFrontend = this.frontends.get(name);
// ๐ Load if not already loaded
if (!microFrontend) {
const loaded = await this.loadMicroFrontend(name);
if (!loaded) {
return false;
}
microFrontend = this.frontends.get(name)!;
}
try {
// ๐๏ธ Mount the micro-frontend
await microFrontend.mount(container, props);
// ๐พ Track mounted frontend
this.mountedFrontends.set(name, container);
console.log(`โ
Micro-frontend mounted successfully: ${name}`);
return true;
} catch (error) {
console.error(`โ Failed to mount micro-frontend ${name}:`, error);
return false;
}
}
// ๐๏ธ Unmount micro-frontend
async unmountMicroFrontend(name: string): Promise<boolean> {
console.log(`๐๏ธ Unmounting micro-frontend: ${name}`);
const microFrontend = this.frontends.get(name);
const container = this.mountedFrontends.get(name);
if (!microFrontend || !container) {
console.warn(`โ ๏ธ Micro-frontend not mounted: ${name}`);
return false;
}
try {
// ๐๏ธ Unmount the micro-frontend
if (microFrontend.unmount) {
await microFrontend.unmount(container);
} else {
// Default unmount: clear container
container.innerHTML = '';
}
// ๐งน Clean up tracking
this.mountedFrontends.delete(name);
console.log(`โ
Micro-frontend unmounted successfully: ${name}`);
return true;
} catch (error) {
console.error(`โ Failed to unmount micro-frontend ${name}:`, error);
return false;
}
}
// ๐ Update micro-frontend props
async updateMicroFrontend(name: string, props: any): Promise<boolean> {
console.log(`๐ Updating micro-frontend: ${name}`);
const microFrontend = this.frontends.get(name);
if (!microFrontend) {
console.warn(`โ ๏ธ Micro-frontend not loaded: ${name}`);
return false;
}
if (!this.mountedFrontends.has(name)) {
console.warn(`โ ๏ธ Micro-frontend not mounted: ${name}`);
return false;
}
try {
if (microFrontend.update) {
await microFrontend.update(props);
console.log(`โ
Micro-frontend updated successfully: ${name}`);
} else {
console.log(`โน๏ธ Micro-frontend ${name} does not support updates`);
}
return true;
} catch (error) {
console.error(`โ Failed to update micro-frontend ${name}:`, error);
return false;
}
}
// ๐บ๏ธ Setup routing system
private setupRouting(): void {
// Listen for route changes
window.addEventListener('popstate', () => {
this.handleRouteChange();
});
// Handle initial route
this.handleRouteChange();
}
// ๐บ๏ธ Handle route changes
private async handleRouteChange(): Promise<void> {
const currentPath = window.location.pathname;
console.log(`๐บ๏ธ Route changed: ${currentPath}`);
// Find matching micro-frontend
let matchedFrontend: string | null = null;
let matchedRoute: string | null = null;
for (const [route, frontendName] of this.routeMap) {
if (this.matchRoute(currentPath, route)) {
matchedFrontend = frontendName;
matchedRoute = route;
break;
}
}
if (matchedFrontend) {
console.log(`๐ฏ Matched micro-frontend: ${matchedFrontend} for route: ${matchedRoute}`);
await this.activateMicroFrontend(matchedFrontend, currentPath);
} else {
console.log(`โ No micro-frontend found for route: ${currentPath}`);
this.showNotFound();
}
}
// ๐ Match route patterns
private matchRoute(path: string, pattern: string): boolean {
// Simple pattern matching (could be enhanced with regex or path-to-regexp)
if (pattern === path) return true;
// Wildcard matching
if (pattern.endsWith('/*')) {
const basePath = pattern.slice(0, -2);
return path.startsWith(basePath);
}
return false;
}
// ๐ Activate micro-frontend for current route
private async activateMicroFrontend(name: string, route: string): Promise<void> {
// ๐๏ธ Unmount all currently mounted frontends
const currentlyMounted = Array.from(this.mountedFrontends.keys());
for (const mountedName of currentlyMounted) {
if (mountedName !== name) {
await this.unmountMicroFrontend(mountedName);
}
}
// ๐๏ธ Mount the new micro-frontend
const container = document.getElementById('micro-frontend-container');
if (container) {
const routeParams = this.extractRouteParams(route);
await this.mountMicroFrontend(name, container, { route, params: routeParams });
}
}
// ๐ Extract route parameters
private extractRouteParams(route: string): Record<string, string> {
// Simple parameter extraction (could be enhanced)
const params: Record<string, string> = {};
const urlParams = new URLSearchParams(window.location.search);
urlParams.forEach((value, key) => {
params[key] = value;
});
return params;
}
// โ Show not found page
private showNotFound(): void {
const container = document.getElementById('micro-frontend-container');
if (container) {
container.innerHTML = `
<div class="not-found">
<h1>404 - Page Not Found</h1>
<p>The requested page could not be found.</p>
</div>
`;
}
}
// ๐ Management utilities
getLoadedMicroFrontends(): string[] {
return Array.from(this.frontends.keys());
}
getMountedMicroFrontends(): string[] {
return Array.from(this.mountedFrontends.keys());
}
getRegisteredMicroFrontends(): string[] {
return Array.from(this.configs.keys());
}
getMicroFrontendInfo(): Array<{
name: string;
route: string;
loaded: boolean;
mounted: boolean;
}> {
return Array.from(this.configs.values()).map(config => ({
name: config.name,
route: config.route,
loaded: this.frontends.has(config.name),
mounted: this.mountedFrontends.has(config.name),
}));
}
}
// ๐ฎ Example micro-frontend implementation
// src/features/user-management/index.ts
export default class UserManagementMicroFrontend implements MicroFrontend {
name = 'user-management';
private container: HTMLElement | null = null;
private userService: any;
async mount(container: HTMLElement, props?: any): Promise<void> {
console.log('๐๏ธ Mounting User Management micro-frontend');
this.container = container;
// ๐ Load required services
const serviceModule = await System.import('./user-service.js');
this.userService = new serviceModule.UserService();
// ๐จ Render the interface
await this.render(props);
// ๐ง Setup event listeners
this.setupEventListeners();
console.log('โ
User Management micro-frontend mounted');
}
async unmount(container: HTMLElement): Promise<void> {
console.log('๐๏ธ Unmounting User Management micro-frontend');
// ๐งน Cleanup event listeners
this.cleanupEventListeners();
// ๐๏ธ Clear container
container.innerHTML = '';
this.container = null;
console.log('โ
User Management micro-frontend unmounted');
}
async update(props: any): Promise<void> {
console.log('๐ Updating User Management micro-frontend', props);
if (this.container) {
await this.render(props);
}
}
getInfo(): any {
return {
name: this.name,
version: '1.0.0',
status: this.container ? 'mounted' : 'unmounted',
userCount: this.userService?.getUserCount() || 0,
};
}
private async render(props?: any): Promise<void> {
if (!this.container) return;
// ๐ฅ Load user data
const users = await this.userService.getUsers();
// ๐จ Create UI
this.container.innerHTML = `
<div class="user-management">
<h2>๐ฅ User Management</h2>
<div class="user-stats">
<p>Total Users: ${users.length}</p>
</div>
<div class="user-actions">
<button id="add-user-btn" class="btn btn-primary">โ Add User</button>
<button id="refresh-btn" class="btn btn-secondary">๐ Refresh</button>
</div>
<div class="user-list">
${this.renderUserList(users)}
</div>
</div>
`;
}
private renderUserList(users: any[]): string {
return users.map(user => `
<div class="user-item" data-user-id="${user.id}">
<div class="user-info">
<h4>${user.name}</h4>
<p>${user.email}</p>
</div>
<div class="user-actions">
<button class="edit-btn" data-user-id="${user.id}">โ๏ธ Edit</button>
<button class="delete-btn" data-user-id="${user.id}">๐๏ธ Delete</button>
</div>
</div>
`).join('');
}
private setupEventListeners(): void {
if (!this.container) return;
// โ Add user button
const addUserBtn = this.container.querySelector('#add-user-btn');
addUserBtn?.addEventListener('click', () => this.handleAddUser());
// ๐ Refresh button
const refreshBtn = this.container.querySelector('#refresh-btn');
refreshBtn?.addEventListener('click', () => this.handleRefresh());
// โ๏ธ Edit buttons
this.container.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.classList.contains('edit-btn')) {
const userId = target.getAttribute('data-user-id');
if (userId) this.handleEditUser(userId);
}
if (target.classList.contains('delete-btn')) {
const userId = target.getAttribute('data-user-id');
if (userId) this.handleDeleteUser(userId);
}
});
}
private cleanupEventListeners(): void {
// Event listeners are automatically cleaned up when container is cleared
}
private async handleAddUser(): Promise<void> {
console.log('โ Adding new user...');
// Implementation for adding user
}
private async handleEditUser(userId: string): Promise<void> {
console.log(`โ๏ธ Editing user: ${userId}`);
// Implementation for editing user
}
private async handleDeleteUser(userId: string): Promise<void> {
console.log(`๐๏ธ Deleting user: ${userId}`);
// Implementation for deleting user
}
private async handleRefresh(): Promise<void> {
console.log('๐ Refreshing user list...');
await this.render();
}
}
๐ Conclusion
Congratulations! Youโve mastered the dynamic art of SystemJS with TypeScript! โก
๐ฏ What Youโve Learned
- โก Dynamic Module Loading: Runtime module loading and dependency resolution
- ๐ Plugin Architecture: Building extensible applications with dynamic plugins
- ๐ Micro-Frontends: Creating scalable frontend architectures with SystemJS
- ๐ Hot Reloading: Dynamic module replacement without page refresh
- ๐ฏ Advanced Patterns: Sophisticated module management and orchestration
- ๐ Production Deployment: Building and optimizing SystemJS applications
๐ Key Benefits
- โก Ultimate Flexibility: Load any module format from anywhere
- ๐ฏ Runtime Adaptability: Applications that evolve based on conditions
- ๐ Extensibility: Easy plugin and micro-frontend integration
- ๐ Development Speed: Hot reloading and dynamic updates
- ๐ Universal Compatibility: Works with all module systems
๐ฅ Best Practices Recap
- ๐ฏ Strategic Loading: Load modules only when needed for better performance
- ๐ Dependency Management: Proper dependency resolution and circular dependency detection
- ๐ Hot Reload Support: Implement proper cleanup for seamless module replacement
- ๐ Error Handling: Robust error handling for dynamic loading failures
- ๐ Production Optimization: Bundle strategies and performance considerations
Youโre now equipped to build incredibly flexible and dynamic TypeScript applications that can adapt, extend, and evolve at runtime! ๐
Happy coding, and may your modules be dynamically delicious! โกโจ