A tiny module loader

Build a small version of what real module systems do.

What to build

define(name, deps, factory) and load(name), which load each module's dependencies first, and run each module only once.

Requirements

  • define stores a module: its name, the names it depends on, and its factory function.
  • load(name) returns the module's value, and works out the value only the first time.
  • To work it out, load the dependencies in order, print running NAME, and call the factory with the dependency values.
  • If a module is not defined, throw an Error with the message unknown module NAME.
  • If a module is reached again while it is still being loaded, throw an Error with the message circular dependency at NAME.
  • A failed load must not stop later loads from working.

Input

Several lines: define NAME DEP1 DEP2 ... (with zero or more dependencies) or load NAME.

Output

The running lines, then for each load, the value or error: MESSAGE.

Constraints

There are from 1 to 100 lines. Names are lower-case letters. The starter builds each module's value for you.

Example

Input:

define a b
define b
load a
load a

Output:

running b
running a
a(b())
a(b())

Why it works

Storing each finished value is what makes a module run only once, even when many others depend on it. The set of names being loaded catches cycles, and finally keeps that set accurate even after a failure.

Examples

Example 1 · Input
define a b
define b
load a
load a
Expected output
running b
running a
a(b())
a(b())
Example 2 · Input
load x
Expected output
error: unknown module x

Hints

Hint 1

Keep the definitions in one Map, the finished values in another, and a Set of names being loaded.

Hint 2

load(name) returns a cached value at once. Otherwise it loads the dependencies, then runs the factory.

Hint 3

Use try ... finally to take a name out of the loading set, even when loading it fails.

Solution

Show one correct solution (try it yourself first)
const modules = new Map();
const finished = new Map();
const loading = new Set();

function define(name, deps, factory) {
  modules.set(name, { deps, factory });
}

function load(name) {
  if (finished.has(name)) {
    return finished.get(name);
  }
  const module = modules.get(name);
  if (!module) {
    throw new Error(`unknown module ${name}`);
  }
  if (loading.has(name)) {
    throw new Error(`circular dependency at ${name}`);
  }
  loading.add(name);
  try {
    const values = module.deps.map((dep) => load(dep));
    console.log(`running ${name}`);
    const value = module.factory(...values);
    finished.set(name, value);
    return value;
  } finally {
    loading.delete(name);
  }
}

let line = prompt();
while (line !== null) {
  const [command, name, ...deps] = line.split(" ");
  if (command === "define") {
    define(name, deps, (...values) => `${name}(${values.join(",")})`);
  } else {
    try {
      console.log(load(name));
    } catch (err) {
      console.log(`error: ${err.message}`);
    }
  }
  line = prompt();
}

Lesson: Modules: splitting a program into files →