Hone logo
Hone
Problems

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:

  1. ModuleLoaderService: Create an Angular service named ModuleLoaderService.
  2. 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 Promise that resolves with the module's exports object.
  3. Handling Exports: The Promise should 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.
  4. Error Handling: Gracefully handle network errors (e.g., invalid URL, resource not found) and execution errors within the loaded module. The Promise should reject with an appropriate error.
  5. 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 ModuleLoaderService must be an Angular injectable service.
  • You can use standard browser APIs like fetch for 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 its src to 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 eval with 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.
Loading editor...
typescript