docs: function assemble(fpath, opts, cb) {
    if (!cb && typeof opts == 'function') {
      cb = opts;
      opts = undefined;
    }
    opts = opts || {};
    if (!opts.importHook) {
      opts.importHook = files.createImportHook();
    }
  
    // Types found in imports. We store them separately to be able to insert them
    // in the correct order in the final attributes.
    var importedTypes = [];
    var attrs, imports;
    opts.importHook(fpath, 'idl', function (err, str) {
      if (err) {
        cb(err);
        return;
      }
      if (!str) {
        // Skipped import (likely already imported).
        cb(null, {});
        return;
      }
      try {
        var protocol = parse(str, opts);
      } catch (err) {
        err.path = fpath; // To help debug which file caused the error.
        cb(err);
        return;
      }
      attrs = protocol.attrs;
      imports = protocol.imports;
      fetchImports();
    });
  
    function fetchImports() {
      var info = imports.shift();
      if (!info) {
        // We are done with this file. We prepend all imported types to this
        // file's and we can return the final result. We also perform a JSON
        // serialization rountrip to remove non-numerical attributes from unions
        // and transform Javadocs into strings.
        if (importedTypes.length) {
          attrs.types = attrs.types ?
            importedTypes.concat(attrs.types) :
            importedTypes;
        }
        cb(null, JSON.parse(JSON.stringify(attrs)));
      } else {
        var importPath = path.join(path.dirname(fpath), info.name);
        if (info.kind === 'idl') {
          assemble(importPath, opts, mergeImportedAttrs);
        } else {
          // We are importing a protocol or schema file.
          opts.importHook(importPath, info.kind, function (err, str) {
            if (err) {
              cb(err);
              return;
            }
            switch (info.kind) {
              case 'protocol':
              case 'schema':
                try {
                  var obj = JSON.parse(str);
                } catch (err) {
                  err.path = importPath;
                  cb(err);
                  return;
                }
                var attrs = info.kind === 'schema' ? {types: [obj]} : obj;
                mergeImportedAttrs(null, attrs);
                break;
              default:
                cb(new Error(f('invalid import kind: %s', info.kind)));
            }
          });
        }
      }
    }
  
    function mergeImportedAttrs(err, importedAttrs) {
      if (err) {
        cb(err);
        return;
      }
      // Merge  first the types (where we don't need to check for duplicates
      // since `parse` will take care of it), then the messages (where we need
      // to, as duplicates will overwrite each other).
      (importedAttrs.types || []).forEach(function (typeAttrs) {
        // Ensure the imported protocol's namespace is inherited correctly (it
        // might be different from the current one).
        if (typeAttrs.namespace === undefined) {
          var namespace = importedAttrs.namespace;
          if (!namespace) {
            var match = /^(.*)\.[^.]+$/.exec(importedAttrs.protocol);
            if (match) {
              namespace = match[1];
            }
          }
          typeAttrs.namespace = namespace || '';
        }
        importedTypes.push(typeAttrs);
      });
      try {
        Object.keys(importedAttrs.messages || {}).forEach(function (name) {
          if (!attrs.messages) {
            attrs.messages = {};
          }
          if (attrs.messages[name]) {
            throw new Error(f('duplicate message: %s', name));
          }
          attrs.messages[name] = importedAttrs.messages[name];
        });
      } catch (err) {
        cb(err);
        return;
      }
      fetchImports(); // Continue importing any remaining imports.
    }
  }