Angular SystemJS Module Loading Challenge
Your task is to implement a custom module loader in an Angular application that mimics the behavior of SystemJS. This challenge focuses on understanding and programmatically loading JavaScript modules within an Angular context, demonstrating a deeper grasp of module systems and dynamic loading strategies. This is useful for scenarios requiring dynamic feature loading, plugins, or integrating third-party code at runtime.
Problem Description
You need to build an Angular service that acts as a SystemJS-like module loader. This service should be capable of dynamically loading JavaScript modules from specified URLs and providing access to their exported members within your Angular application.
What needs to be achieved:
- Create an Angular service that can load a JavaScript module given a URL.
- The loader should be able to resolve module dependencies (though for this challenge, we'll simplify this by assuming modules don't have complex interdependencies beyond what SystemJS handles natively or what you can mock).
- The service should expose a mechanism to retrieve exported members from the loaded module.
Key Requirements:
ModuleLoaderService: Create an Angular service namedModuleLoaderService.loadModule(url: string): Promise<any>: Implement a method that takes a URL to a JavaScript module. This method should:- Fetch the JavaScript code from the provided URL.
- Execute the JavaScript code in a way that makes its exports accessible (you can simulate SystemJS's
System.import()behavior). - Return a
Promisethat resolves with the module's exports object.
- Handling Exports: The
Promiseshould resolve with an object containing the named exports of the loaded module. For simplicity in this challenge, assume modules export a single default object or named properties. - Error Handling: Gracefully handle network errors (e.g., invalid URL, resource not found) and execution errors within the loaded module. The
Promiseshould reject with an appropriate error. - Angular Integration: The service should be injectable into Angular components.
Expected Behavior:
When a component calls moduleLoaderService.loadModule('path/to/my/module.js'), it should receive a Promise. Upon successful loading and execution of the module, the Promise should resolve with an object representing the module's exports. For example, if my-module.js contains export const greeting = 'Hello'; export function sayHi() { console.log(greeting); }, the resolved Promise should yield an object like { greeting: 'Hello', sayHi: [Function] }.
Important Edge Cases:
- Invalid URLs: The loader should handle URLs that are malformed or point to non-existent resources.
- Syntax Errors in Modules: If the loaded JavaScript has syntax errors, the loader should catch and report them.
- Runtime Errors in Modules: If the loaded JavaScript throws errors during execution, these should also be caught and reported.
- Security: For this challenge, we won't implement full sandboxing but are mindful that executing arbitrary code has security implications.
Examples
Example 1: Loading a simple module with a named export
Module (./assets/modules/hello.js):
export const message = "World";
Angular Component Usage:
import { Component } from '@angular/core';
import { ModuleLoaderService } from './module-loader.service';
@Component({
selector: 'app-hello',
template: `<p>{{ loadedMessage }}</p>`
})
export class HelloComponent {
loadedMessage: string | undefined;
constructor(private moduleLoaderService: ModuleLoaderService) {}
async ngOnInit() {
try {
const module = await this.moduleLoaderService.loadModule('./assets/modules/hello.js');
this.loadedMessage = module.message; // Accessing the named export
} catch (error) {
console.error("Failed to load module:", error);
}
}
}
Expected Output (in the component's template):
<p>World</p>
Explanation: The loadModule method fetches hello.js, executes it, and returns its exports. The component then accesses the message export and displays it.
Example 2: Loading a module with a function export
Module (./assets/modules/calculator.js):
export function add(a, b) {
return a + b;
}
export const subtract = (a, b) => a - b;
Angular Component Usage:
import { Component } from '@angular/core';
import { ModuleLoaderService } from './module-loader.service';
@Component({
selector: 'app-calculator-test',
template: `<p>Sum: {{ sum }}</p><p>Difference: {{ difference }}</p>`
})
export class CalculatorTestComponent {
sum: number | undefined;
difference: number | undefined;
constructor(private moduleLoaderService: ModuleLoaderService) {}
async ngOnInit() {
try {
const calculatorModule = await this.moduleLoaderService.loadModule('./assets/modules/calculator.js');
this.sum = calculatorModule.add(5, 3);
this.difference = calculatorModule.subtract(10, 4);
} catch (error) {
console.error("Failed to load calculator module:", error);
}
}
}
Expected Output (in the component's template):
<p>Sum: 8</p>
<p>Difference: 6</p>
Explanation: The calculator.js module is loaded. The component then calls the add and subtract functions exported by the module and displays their results.
Example 3: Handling a non-existent module
Angular Component Usage:
import { Component } from '@angular/core';
import { ModuleLoaderService } from './module-loader.service';
@Component({
selector: 'app-error-handling',
template: `<p>Status: {{ loadStatus }}</p>`
})
export class ErrorHandlingComponent {
loadStatus: string = 'Loading...';
constructor(private moduleLoaderService: ModuleLoaderService) {}
async ngOnInit() {
try {
await this.moduleLoaderService.loadModule('./assets/modules/nonexistent.js');
this.loadStatus = 'Module loaded (unexpected)';
} catch (error) {
console.error("Caught expected error:", error);
this.loadStatus = 'Failed to load module as expected';
}
}
}
Expected Output (in the component's template):
<p>Failed to load module as expected</p>
And a console log:
Caught expected error: Error: Failed to load module: ./assets/modules/nonexistent.js (or similar network/fetch error)
Explanation: Attempting to load a module from an invalid URL results in a rejected Promise, which is caught by the component's catch block.
Constraints
- The solution must be implemented in TypeScript.
- The
ModuleLoaderServicemust be an Angular injectable service. - You can use standard browser APIs like
fetchfor retrieving module code. - For simulating SystemJS's execution environment, you can use techniques like creating a script tag dynamically or using
eval(with caution, or preferably a safer alternative if available in your environment). - Assume all loaded modules are plain JavaScript files that use ES module syntax (
export). - You do not need to implement a full SystemJS bundle or complex dependency graph resolution. Focus on loading a single module and accessing its exports.
Notes
- Consider how you will execute the fetched JavaScript code. You can create a temporary
<script>tag, set itssrcto a data URL containing the fetched code, and append it to the DOM. The execution of this script will make its exports available. - To capture exports, you might need a mechanism to intercept the module's definition before it's finalized by the JavaScript engine, or use a shim that exposes exports globally or to a specific variable that your service can access. A common pattern for simple dynamic imports without a full bundler is to use
evalwith a wrapper. - Think about how to map module names (like import paths in your Angular code) to actual URLs if you were building a more complete loader. For this challenge, direct URLs are sufficient.
- The goal is to understand the principles of dynamic module loading, not to replicate SystemJS's entire feature set.
- Your service should be designed to be testable.