view weather_server/typescript/amd/mad-amd.ts @ 15:df3e0534c994

Tighten up MADRegistry: - Use only one map for registry. - Make the user construct it, to avoid modifying global state.
author Paul Fisher <paul@pfish.zone>
date Fri, 11 Oct 2019 20:50:50 -0400
parents dd77a7ee02c1
children
line wrap: on
line source

/** Type of the AMD factory function. */
type FactoryFunction = (...deps: unknown[]) => void;

/** An individual AMD module. */
interface Module {
    /** The names of the module's dependencies. */
    deps: string[];
    /**
     * The function that, when called with the module's list of dependencies,
     * creates the module.
     */
    factory: FactoryFunction;
    /**
     * When null, an indication that the module has not yet been reified.
     * When non-null, the members that the module has exported.
     */
    exports: {}|null;
}

/**
 * Minimal AMD Dumb Registry: the dumbest possible implementation of AMD,
 * to handle only the code that `tsc -m amd` produces.
 *
 * Supports `require` of absolute paths and `define`s.
 */
class MADRegistry {
    /** The registry itself, mapping from name to module. */
    private readonly mods = new Map<string, Module>();

    /**
     * The (subset of) the AMD `define` function we implement.
     *
     * This supports
     *
     * - Absolute paths
     * - `exports`-based module construction
     *
     * @param name The name of the module to define.
     * @param deps The dependencies of the module. Must be explicit.
     * @param factory The module's factory function.
     */
    define(name: string, deps: string[], factory: FactoryFunction) {
        this.mods.set(name, {deps, factory, exports: null});
    }

    /**
     * The (subset of) the AMD `require` function we implement.
     * Only `require(dep)` is exposed to users; `require(dep, srcMod)`
     * is internal-only.
     *
     * - Does not support relative paths.
     * - Does not define `require.amd`, because we do not fully support AMD
     *   and don't want to give the impression that we do.
     *
     * @param dep The name of the dependency.
     * @param srcMod The module whence the dependency was requested.
     *     Used for when the name `exports` is required.
     */
    require(dep: string, srcMod?: Module): {} {
        if (dep === 'require') {
            return (child: string) => this.require(child, srcMod);
        }
        if (dep === 'exports') {
            if (!srcMod) throw new Error('Internal consistency error.');
            // We know this is safe because a module can only ever require
            // its own exports after it is itself required.
            return srcMod.exports!;
        }
        const mod = this.mods.get(dep);
        if (!mod) throw new Error('Undefined module.');
        // If we've required the module before, return its exports.
        if (mod.exports) return mod.exports;
        // Otherwise, we need to prepare the module and require its parents.
        mod.exports = {};
        const deps = mod.deps.map(child => this.require(child, mod));
        mod.factory(...deps);
        return mod.exports;
    }

    /**
     * Installs this registry into the given object, usually `window` or `self`.
     * For usage with a separately-compiled JS file, do:
     *
     * ```typescript
     * new MADRegistry().install();
     * ```
     */
    install(to: any) {
        to['define'] =
            (name: string, deps: string[], factory: FactoryFunction) =>
                this.define(name, deps, factory);
        to['require'] = (dep: string) => this.require(dep);
    }
}