Home Manual Reference Source Test Repository

src/di.js

import {getAlias} from './alias';
import {name as pkgName} from '../package.json';

/**
 * The dependency injection (DI) container of the application.
 * @type {Map}
 */
const CONTAINER = new Map;

/**
 * Creates a new object using the dependency injection (DI) container of the application.
 *
 * You may view this function as an enhanced version of the `new` operator. The function supports creating an object based on one of the following forms:
 * - a string: a class alias representing the location and the class name of the object to be created.
 * - a configuration object: the object must contain a `class` element which is treated as the class alias, and the rest of the name-value pairs will be used to initialize the corresponding object properties.
 *
 * @param {string|object} type The object type: a class alias or a configuration object.
 * @param {...*} params The constructor parameters.
 * @return {*} The newly created object.
 * @throws {TypeError} The object type is not supported.
 */
export function createObject(type, ...params) {
  let Type;
  switch (typeof type) {
    case 'object':
      if (!type || typeof type.class != 'string')
        throw new TypeError('Configuration must be an object containing a "class" property.');

      Type = registerType(type.class);
      let instance = new Type(...params);
      delete type.class;

      if (typeof instance.init == 'function') instance.init(type);
      else Object.assign(instance, type);
      return instance;

    case 'string':
      Type = registerType(type);
      return new Type(...params);

    default:
      throw new TypeError(`Unsupported configuration type: ${typeof type}`);
  }
}

/**
 * Registers a type in the dependency injection (DI) container of the application.
 *
 * A type is registered using a class alias specifying the location and the name of the type to be registered.
 * For example:
 * - `@app/components/FooBar`: will be resolved to the `FooBar` class exported by the `@app/components.js` module, or the `@app/components/index.js` module.
 * - `@core/FooBar`: will be resolved to the `FooBar` class exported by this package.
 * - `@npm/foo/FooBar`: will be resolved to the `FooBar` class exported by the `foo` package located in the `node_modules` folder.
 * - `@npm/@foo/bar/FooBar`: will be resolved to the `FooBar` class exported by the `@foo/bar` package located in the `node_modules` folder.
 *
 * @param {string} alias The class alias representing the location and the name of the type to be registered.
 * @return {*} The resolved type.
 * @throws {TypeError} The specified class alias is invalid, or the corresponding type is not found.
 */
export function registerType(alias) {
  if (typeof alias != 'string' || !alias.length) throw new TypeError('The specified class alias is empty.');

  if (!alias.startsWith('@')) alias = `@${alias}`;
  if (CONTAINER.has(alias)) return CONTAINER.get(alias);

  let parts = alias.split('/');
  if (parts.length < 2) throw new TypeError(`The type name is missing from the "${alias}" class alias.`);

  let className = parts.pop();
  let path;

  switch (parts[0]) {
    case '@core':
      parts.shift();
      parts.unshift(pkgName);
      path = parts.join('/');
      break;

    case '@npm':
      parts.shift();
      path = parts.join('/');
      break;

    default:
      path = getAlias(parts.join('/'));
      break;
  }

  try {
    const module = require(path);
    if (!(className in module)) throw new Error(`The "${className}" type is not found in module: "${path}"`);
    CONTAINER.set(alias, module[className]);
  }

  catch (err) {
    throw new TypeError(err.message);
  }

  return CONTAINER.get(alias);
}