function expandFile(file, outDir, outStream, options, includeStack) {
  options = options || {pre: nop, visit: nop, post: nop};
  options.pre(file, outDir, outStream);
  var lines = fs.readFileSync(file, 'utf-8').split('\n');
  // Last item empty means that 'file' ends in a newline.
  // Remove to prevent printing an extra empty line later.
  if (lines[lines.length - 1] === '')
    lines.pop();

  // Check for runaway #include; cycles and recursion limit.
  var found;
  includeStack = includeStack || [];
  if ((found = includeStack.indexOf(file)) >= 0) {
    throw new Error('#include cycle detected: ' + includeStack.slice(found));
  }
  includeStack.push(file);
  if (includeStack.length > INCLUDE_LIMIT) {
    throw new Error('#include limit exceeded: ' + includeStack);
  }

  var resolvedFile = path.relative(outDir, file);
  var fileDir = path.dirname(file);

  var includeFile = file;
  var length = lines.length, match;
  for (var i = 0; i < length; i++) {
    if (match = lines[i].match(/\s*\/\/ #include (.*)$/)) {
      includeFile = path.relative('.', path.resolve(fileDir, match[1]));
      options.visit(file, outDir, outStream, includeFile);
      expandFile(includeFile, outDir, outStream, options, includeStack);
      continue;
    }
    // Print for every change to a new file, including the initial file.
    // Don't print unless actual lines from the file are being printed
    // (#include directives don't count).
    if (includeFile !== null) {
      outStream.write(format('// #line %s %s\n', i + 1, resolvedFile));
      includeFile = null;
    }
    outStream.write(lines[i] + '\n');
  }

  includeStack.pop(file);

  options.post(file, outDir, outStream);
}
Outputs the recursive expansion of the #include directives in |file| to
|outStream|, while printing #line directives relative to |outDir|.
@param {string} file
@param {string} outDir
@param {WriteStream} outStream
@param {Object} options
@param {Array=} includeStack