interlockjs/interlock

View on GitHub
src/compile/index.js

Summary

Maintainability
A
0 mins
Test Coverage
import { assign, chain, flatten, values } from "lodash";
import Promise from "bluebird";

import { pluggable, getBaseContext } from "pluggable";

import { constructBundleAst } from "./construct";
import getModuleSeeds from "./modules/get-seeds";
import generateModuleMaps from "./modules/generate-maps";
import compileModules from "./modules/compile";
import generateBundles from "./bundles/generate";
import generateRawBundles from "./bundles/generate-raw";

import fcachePlugin from "../optimizations/file-cache";


/**
 * Given an array of bundles, generate a lookup dictionary of module hashes
 * to the destination path of the bundles that contains them.
 *
 * @param  {Array}   bundles  Compiled bundles.
 *
 * @return {Object}           moduleHash-to-URL lookup dictionary.
 */
export const getUrls = pluggable(function getUrls (bundles) {
  return bundles.reduce((urls, bundle) => {
    bundle.moduleHashes.forEach(hash => urls[hash] = `/${bundle.dest}`);
    return urls;
  }, {});
});

/**
 * Given a compiled bundle and moduleHash-to-URL lookup object, output
 * the same bundle with generated AST.
 *
 * @param  {Object} bundle   Fully compiled bundle, ready to be outputed.
 * @param  {Object} urls     moduleHash-to-URL lookup dictionary.
 *
 * @return {Object}          Bundle with new `ast` property.
 */
export const constructBundle = pluggable(function constructBundle (bundle, urls) {
  return this.constructBundleAst({
    modules: bundle.modules,
    includeRuntime: bundle.includeRuntime,
    urls: bundle.isEntryPt ? urls : null,
    entryModuleId: bundle.isEntryPt && bundle.module && bundle.module.id || null
  })
    .then(ast => assign({}, bundle, { ast }));
}, { constructBundleAst });

/**
 * Given an array of compiled bundles and a moduleHash-to-URL lookup dictionary,
 * generate a new array of bundles with new `ast` and `raw` properties.
 *
 * Some compiled bundles (as internally represented) will result in more than
 * one output file.  The canonical example of this is a JS file and its source-map.
 * Plugins may also implement mechanisms to output multiple files per bundle.
 *
 * This one-to-many relationship is defined by the generateRawBundles method, which
 * may output an array of raw bundles.
 *
 * @param  {Array}  bundlesArr  Compiled bundles.
 * @param  {Object} urls        moduleHash-to-URL lookup dictionary.
 *
 * @return {Array}              Bundles with new `raw` properties.
 */
export const emitRawBundles = pluggable(function emitRawBundles (bundlesArr, urls) {
  return Promise.all(bundlesArr.map(bundle =>
    this.constructBundle(bundle, urls)
      .then(this.generateRawBundles)
  ))
    // generateRawBundles returns arrays of bundles.  This allows, for example, a
    // source map to also be emitted along with its bundle JS.
    .then(flatten);
}, { constructBundle, generateRawBundles });

/**
 * Reduces an array of compiled bundles into a compilation object.  This compilation
 * object will have three key/value pairs:
 *
 * - **cache:**    populated from the compilation process
 * - **bundles:**  a mapping of destination paths to `raw` code
 * - **opts:**     the original options passed to the compilation
 *
 * @param  {Array}   bundles   Compiled bundles, generated by generateBundles.
 *
 * @return {Promise}           Compilation object.
 */
export const buildOutput = pluggable(function buildOutput (bundles) {
  return this.getUrls(bundles)
    .then(urls => this.emitRawBundles(bundles, urls))
    .then(rawBundles => chain(rawBundles)
        .map(rawBundle => [rawBundle.dest, rawBundle])
        .fromPairs()
        .value())
    .then(bundlesByDest => ({
      bundles: bundlesByDest,
      opts: this.opts,
      cache: this.cache
    }));
}, { getUrls, emitRawBundles });

/**
 * Loads, transforms, and bundles an application using the provided options.
 * Modules are collected and transformed, bundles are formed from those modules,
 * and those bundles are finally converted into a format that can be written
 * to disk or served over HTTP.
 *
 * @return {Promise}    Resolves to an object with three properties: `bundles`,
 *                      `opts`, and `cache`.
 */
const compile = pluggable(function compile () {
  return this.getModuleSeeds()
    .then(moduleSeeds => Promise.all([
      moduleSeeds,
      this.compileModules(values(moduleSeeds)).then(this.generateModuleMaps.bind(this))
    ]))
    .then(([moduleSeeds, moduleMaps]) => this.generateBundles(moduleSeeds, moduleMaps))
    .then(this.buildOutput);
}, { getModuleSeeds, compileModules, generateModuleMaps, generateBundles, buildOutput });


export default function (opts) {
  const plugins = [].concat(opts.plugins);

  if (opts.fcache) {
    const cacheDir = opts.fcache === true ? null : opts.fcache;
    plugins.push(fcachePlugin({ cacheDir }));
  }

  return compile.call(getBaseContext({
    cache: {
      modulesByAbsPath: Object.create(null)
    },
    opts: Object.freeze(opts)
  }, plugins));
}