import {
  SourceMapConsumer,
  SourceMapGenerator,
} from 'source-map';
import coffee from 'coffeescript';
import { BabelCompiler } from 'meteor/babel-compiler';

// The CoffeeScript compiler overrides Error.prepareStackTrace, mostly for the
// use of coffee.run which we don't use.  This conflicts with the tool's use of
// Error.prepareStackTrace to properly show error messages in linked code.
// Restore the tool's one after CoffeeScript clobbers it at import time.
if (Error.METEOR_prepareStackTrace) {
  Error.prepareStackTrace = Error.METEOR_prepareStackTrace;
}

Plugin.registerCompiler({
  extensions: ['coffee', 'litcoffee', 'coffee.md']
}, () => new CoffeeCompiler());

// The CompileResult for this CachingCompiler is a {source, sourceMap} object.

export class CoffeeCompiler extends CachingCompiler {
  constructor() {
    super({
      compilerName: 'coffeescript',
      defaultCacheSize: 1024*1024*10,
    });

    this.babelCompiler = new BabelCompiler({
      // Prevent Babel from importing helpers from babel-runtime, since
      // the CoffeeScript plugin does not imply the modules package, which
      // means require may not be defined. Note that this in no way
      // prevents CoffeeScript projects from using the modules package and
      // putting require or import statements within backticks; it just
      // won't happen automatically because of Babel.
      runtime: false
    });
  }

  _getCompileOptions(inputFile) {
    return {
      bare: true,
      filename: inputFile.getPathInPackage(),
      literate: inputFile.getExtension() !== 'coffee',
      // Return a source map.
      sourceMap: true,
      // This becomes the "file" field of the source map.
      generatedFile: '/' + this._outputFilePath(inputFile),
      // This becomes the "sources" field of the source map.
      sourceFiles: [inputFile.getDisplayPath()],
    };
  }

  _outputFilePath(inputFile) {
    return inputFile.getPathInPackage() + '.js';
  }

  getCacheKey(inputFile) {
    return [
      inputFile.getSourceHash(),
      inputFile.getDeclaredExports(),
      this._getCompileOptions(inputFile),
    ];
  }

  setDiskCacheDirectory(cacheDir) {
    this.babelCompiler.setDiskCacheDirectory(cacheDir);
    return super.setDiskCacheDirectory(cacheDir);
  }

  compileOneFile(inputFile) {
    const source = inputFile.getContentsAsString();
    const compileOptions = this._getCompileOptions(inputFile);

    let output;
    try {
      output = coffee.compile(source, compileOptions);
    } catch (e) {
      inputFile.error({
        message: e.message,
        line: e.location && (e.location.first_line + 1),
        column: e.location && (e.location.first_column + 1)
      });
      return null;
    }

    let sourceMap = JSON.parse(output.v3SourceMap);
    sourceMap.sourcesContent = [source];

    output.js = stripExportedVars(
      output.js,
      inputFile.getDeclaredExports().map(e => e.name)
    );

    // CoffeeScript contains a handful of features that output as ES2015+,
    // such as modules, generator functions, for…of, and tagged template
    // literals. Because they’re too varied to detect, pass all CoffeeScript
    // compiler output through the Babel compiler.
    const doubleRoastedCoffee =
      this.babelCompiler.processOneFileForTarget(inputFile, output.js);

    if (doubleRoastedCoffee != null &&
        doubleRoastedCoffee.data != null) {
      output.js = doubleRoastedCoffee.data;

      const coffeeSourceMap = doubleRoastedCoffee.sourceMap;

      if (coffeeSourceMap) {
        // Reference the compiled CoffeeScript file so that `applySourceMap`
        // below can match it with the source map produced by the CoffeeScript
        // compiler.
        coffeeSourceMap.sources[0] = '/' + this._outputFilePath(inputFile);

        // Combine the original CoffeeScript source map with the one
        // produced by this.babelCompiler.processOneFileForTarget.
        const smg = SourceMapGenerator.fromSourceMap(
          new SourceMapConsumer(coffeeSourceMap)
        );
        smg.applySourceMap(new SourceMapConsumer(sourceMap));
        sourceMap = smg.toJSON();
      } else {
        // If the .coffee file is contained by a node_modules directory,
        // then BabelCompiler will not transpile it, and there will be
        // no sourceMap, but that's fine because the original
        // CoffeeScript sourceMap will still be valid.
      }
    }

    return addSharedHeader(output.js, sourceMap);
  }

  addCompileResult(inputFile, sourceWithMap) {
    inputFile.addJavaScript({
      path: this._outputFilePath(inputFile),
      sourcePath: inputFile.getPathInPackage(),
      data: sourceWithMap.source,
      sourceMap: sourceWithMap.sourceMap,
      bare: inputFile.getFileOptions().bare
    });
  }

  compileResultSize(sourceWithMap) {
    return sourceWithMap.source.length +
      this.sourceMapSize(sourceWithMap.sourceMap);
  }
}

function stripExportedVars(source, exports) {
  if (!exports || !exports.length)
    return source;
  const lines = source.split("\n");

  // We make the following assumptions, based on the output of CoffeeScript
  // 1.7.1.
  //   - The var declaration in question is not indented and is the first such
  //     var declaration.  (CoffeeScript only produces one var line at each
  //     scope and there's only one top-level scope.)  All relevant variables
  //     are actually on this line.
  //   - The user hasn't used a ###-comment containing a line that looks like
  //     a var line, to produce something like
  //        /* bla
  //        var foo;
  //        */
  //     before an actual var line.  (ie, we do NOT attempt to figure out if
  //     we're inside a /**/ comment, which is produced by ### comments.)
  //   - The var in question is not assigned to in the declaration, nor are any
  //     other vars on this line. (CoffeeScript does produce some assignments
  //     but only for internal helpers generated by CoffeeScript, and they end
  //     up on subsequent lines.)
  // XXX relax these assumptions by doing actual JS parsing (eg with jsparse).
  //     I'd do this now, but there's no easy way to "unparse" a jsparse AST.
  //     Or alternatively, hack the compiler to allow us to specify unbound
  //     symbols directly.

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    const match = /^var (.+)([,;])$/.exec(line);
    if (!match)
      continue;

    // If there's an assignment on this line, we assume that there are ONLY
    // assignments and that the var we are looking for is not declared. (Part
    // of our strong assumption about the layout of this code.)
    if (match[1].indexOf('=') !== -1)
      continue;

    // We want to replace the line with something no shorter, so that all
    // records in the source map continue to point at valid
    // characters.
    function replaceLine(x) {
      if (x.length >= lines[i].length) {
        lines[i] = x;
      } else {
        lines[i] = x + new Array(1 + (lines[i].length - x.length)).join(' ');
      }
    }

    let vars = match[1].split(', ').filter(v => exports.indexOf(v) === -1);
    if (vars.length) {
      replaceLine('var ' + vars.join(', ') + match[2]);
    } else {
      // We got rid of all the vars on this line. Drop the whole line if this
      // didn't continue to the next line, otherwise keep just the 'var '.
      if (match[2] === ';')
        replaceLine('');
      else
        replaceLine('var');
    }
    break;
  }

  return lines.join('\n');
}

function addSharedHeader(source, sourceMap) {
  // We want the symbol "share" to be visible to all CoffeeScript files in the
  // package (and shared between them), but not visible to JavaScript
  // files. (That's because we don't want to introduce two competing ways to
  // make package-local variables into JS ("share" vs assigning to non-var
  // variables).) The following hack accomplishes that: "__coffeescriptShare"
  // will be visible at the package level and "share" at the file level.  This
  // should work both in "package" mode where __coffeescriptShare will be added
  // as a var in the package closure, and in "app" mode where it will end up as
  // a global.
  //
  // This ends in a newline to make the source map easier to adjust.
  const header = ("__coffeescriptShare = typeof __coffeescriptShare === 'object' " +
                  "? __coffeescriptShare : {}; " +
                  "var share = __coffeescriptShare;\n");

  // If the file begins with "use strict", we need to keep that as the first
  // statement.
  const processedSource = source.replace(/^(?:((['"])use strict\2;)\n)?/, (match, useStrict) => {
    if (match) {
      // There's a "use strict"; we keep this as the first statement and insert
      // our header at the end of the line that it's on. This doesn't change
      // line numbers or the part of the line that previous may have been
      // annotated, so we don't need to update the source map.
      return useStrict + '  ' + header;
    } else {
      // There's no use strict, so we can just add the header at the very
      // beginning. This adds a line to the file, so we update the source map to
      // add a single un-annotated line to the beginning.
      sourceMap.mappings = ';' + sourceMap.mappings;
      return header;
    }
  });
  return {
    source: processedSource,
    sourceMap: sourceMap
  };
}


// Tests the actual cache logic used by coffeescript.

selftest.define("compiler plugin caching - coffee", () => {

var s = new Sandbox({ fakeMongo: true });

// Ask them to print out when they build a file (instead of using it from the

// cache) as well as when they load cache from disk.

keep keep keep replace keep keep keep keep keep


import assert from "assert";

import "github";
import "aws-sdk";
import "stripe";
import "mssql";

import winston from "winston";
import * as winstonNamespace from "winston";
assert.strictEqual(typeof winston.default, "object");
assert.strictEqual(typeof winston.transports.Console, "function");
assert.strictEqual(winstonNamespace.default, winston);

// This package has a native .node module as the "main" property of its
// package.json file. (#7947)
import { start } from "idle-gc";
assert.strictEqual(typeof start, "function");

assert.strictEqual(Meteor.isServer, true);
assert.strictEqual(Meteor.isClient, false);

export default module.id;


keep keep keep keep keep replace keep keep keep

var _ = require('underscore');
var selftest = require('../tool-testing/selftest.js');
var files = require('../fs/files.js');
import { getUrl } from '../utils/http-helpers.js';
import { sleepMs } from '../utils/utils.js';

var Sandbox = selftest.Sandbox;

var MONGO_LISTENING =
  { stdout: " [initandlisten] waiting for connections on port" };

function startRun(sandbox) {
  var run = sandbox.run();
  run.match("myapp");
  run.matchBeforeExit("Started proxy");
  run.tellMongo(MONGO_LISTENING);
  run.matchBeforeExit("Started MongoDB");
  run.waitSecs(15);
  return run;
};

// Tests the actual cache logic used by coffeescript.
selftest.define("compiler plugin caching - coffee", () => {
  var s = new Sandbox({ fakeMongo: true });
  s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");

  s.createApp("myapp", "caching-coffee");
  s.cd("myapp");
  // Ask them to print out when they build a file (instead of using it from the
  // cache) as well as when they load cache from disk.
  s.set('METEOR_COFFEESCRIPT_CACHE_DEBUG', 't');
  var run = startRun(s);

  // First program built (server or web.browser) compiles everything.
  run.match('CACHE(coffeescript): Ran (#1) on: ' + JSON.stringify(
    ['/f1.coffee', '/f2.coffee', '/f3.coffee', '/packages/local-pack/p.coffee']
  ));
  // Second program doesn't need to compile anything because compilation works
  // the same on both programs.
  run.match("CACHE(coffeescript): Ran (#2) on: []");
  // App prints this:
  run.match("Coffeescript X is 2 Y is 1 FromPackage is 4");

  s.write("f2.coffee", "share.Y = 'Y is 3'\n");
  // Only recompiles f2.
  run.match('CACHE(coffeescript): Ran (#3) on: ["/f2.coffee"]');
  // And other program doesn't even need to do f2.
  run.match("CACHE(coffeescript): Ran (#4) on: []");
  // Program prints this:
  run.match("Coffeescript X is 2 Y is 3 FromPackage is 4");

  // Force a rebuild of the local package without actually changing the
  // coffeescript file in it. This should not require us to coffee.compile
  // anything (for either program).
  s.append("packages/local-pack/package.js", "\n// foo\n");
  run.match("CACHE(coffeescript): Ran (#5) on: []");
  run.match("CACHE(coffeescript): Ran (#6) on: []");
  run.match("Coffeescript X is 2 Y is 3 FromPackage is 4");

  // But writing to the actual source file in the local package should
  // recompile.
  s.write("packages/local-pack/p.coffee", "FromPackage = 'FromPackage is 5'");
  run.match('CACHE(coffeescript): Ran (#7) on: ["/packages/local-pack/p.coffee"]');
  run.match('CACHE(coffeescript): Ran (#8) on: []');
  run.match("Coffeescript X is 2 Y is 3 FromPackage is 5");

  // We never should have loaded cache from disk, since we only made
  // each compiler once and there were no cache files at this point.
  run.forbid('CACHE(coffeescript): Loaded');

  // Kill the run. Change one coffee file and re-run.
  run.stop();
  s.write("f2.coffee", "share.Y = 'Y is edited'\n");
  run = startRun(s);

  // This time there's a cache to load!
  run.match('CACHE(coffeescript): Loaded /packages/local-pack/p.coffee');
  run.match('CACHE(coffeescript): Loaded /f1.coffee');
  run.match('CACHE(coffeescript): Loaded /f3.coffee');
  // And we only need to re-compiler the changed file, even though we restarted.
  run.match('CACHE(coffeescript): Ran (#1) on: ["/f2.coffee"]');
  run.match('CACHE(coffeescript): Ran (#2) on: []');

  run.match('Coffeescript X is 2 Y is edited FromPackage is 5');

  run.stop();
});

// Tests the actual cache logic used by less and stylus.
['less', 'stylus'].forEach((packageName) => {
  const extension = packageName === 'stylus' ? 'styl' : packageName;

  selftest.define("compiler plugin caching - " + packageName, () => {
    var s = new Sandbox({ fakeMongo: true });
    s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");

    s.createApp("myapp", "caching-" + packageName);
    s.cd("myapp");
    // Ask them to print out when they build a file (instead of using it from
    // the cache) as well as when they load cache from disk.
    s.set(`METEOR_${ packageName.toUpperCase() }_CACHE_DEBUG`, "t");
    var run = startRun(s);

    const cacheMatch = selftest.markStack((message) => {
      run.match(`CACHE(${ packageName }): ${ message }`);
    });

    // First program built (web.browser) compiles everything.
    cacheMatch('Ran (#1) on: ' + JSON.stringify([
      // Though files in imports directories are compiled, they are marked
      // as lazy so they will not be loaded unless imported.
      "/imports/dotdot." + extension,
      "/subdir/nested-root." + extension,
      "/top." + extension
    ]));
    // There is no render execution in the server program, because it has
    // archMatching:'web'.  We'll see this more clearly when the next call later
    // is "#2" --- we didn't miss a call!
    // App prints this:
    run.waitSecs(15);
    run.match("Hello world");

    // Check that the CSS is what we expect.
    var checkCSS = selftest.markStack((borderStyleMap) => {
      var builtBrowserProgramDir = files.pathJoin(
        s.cwd, '.meteor', 'local', 'build', 'programs', 'web.browser');
      var cssFile = _.find(
        files.readdir(
          files.pathJoin(s.cwd, '.meteor/local/build/programs/web.browser')),
        path => path.match(/\.css$/)
      );
      selftest.expectTrue(cssFile);
      var actual = s.read(
        files.pathJoin('.meteor/local/build/programs/web.browser', cssFile));
      actual = actual.replace(/\s+/g, ' ');  // simplify whitespace
      var expected = _.map(borderStyleMap, (style, className) => {
        return '.' + className + " { border-style: " + style + "; }";
      }).join(' ');
      selftest.expectEqual(actual, expected);
    });
    var expectedBorderStyles = {
      el0: "dashed", el1: "dotted", el2: "solid", el3: "groove", el4: "ridge"};
    checkCSS(expectedBorderStyles);

    // Force a rebuild of the local package without actually changing the
    // preprocessor file in it. This should not require us to render anything.
    s.append("packages/local-pack/package.js", "\n// foo\n");
    cacheMatch('Ran (#2) on: []');
    run.waitSecs(15);
    run.match("Hello world");

    function setVariable(variableName, value) {
      switch (packageName) {
      case 'less':
        return `@${ variableName }: ${ value };\n`;
      case 'stylus':
        return `$${ variableName } = ${ value }\n`;
      }
    }
    function importLine(fileWithoutExtension) {
      switch (packageName) {
      case 'less':
        return `@import "${ fileWithoutExtension }.less";\n`;
      case 'stylus':
        return `@import "${ fileWithoutExtension }.styl"\n`;
      }
    }

    // Writing to a single file only re-renders the root that depends on it.
    s.write('packages/local-pack/p.' + extension,
            setVariable('el4-style', 'inset'));
    expectedBorderStyles.el4 = 'inset';
    cacheMatch(`Ran (#3) on: ["/top.${ extension }"]`);
    run.match("Client modified -- refreshing");
    checkCSS(expectedBorderStyles);

    // This works for changing a root too.
    s.write('subdir/nested-root.' + extension,
            '.el0 { border-style: double; }\n');
    expectedBorderStyles.el0 = 'double';
    cacheMatch(`Ran (#4) on: ["/subdir/nested-root.${ extension }"]`);
    run.match("Client modified -- refreshing");
    checkCSS(expectedBorderStyles);

    // Adding a new root works too.
    s.write('yet-another-root.' + extension,
            '.el6 { border-style: solid; }\n');
    expectedBorderStyles.el6 = 'solid';
    cacheMatch(`Ran (#5) on: ["/yet-another-root.${ extension }"]`);
    run.match("Client modified -- refreshing");
    checkCSS(expectedBorderStyles);

    // We never should have loaded cache from disk, since we only made
    // each compiler once and there were no cache files at this point.
    run.forbid('CACHE(${ packageName }): Loaded');

    // Kill the run. Change one file and re-run.
    run.stop();
    s.write('packages/local-pack/p.' + extension,
            setVariable('el4-style', 'double'));
    expectedBorderStyles.el4 = 'double';
    run = startRun(s);

    // This time there's a cache to load!  Note that for
    // MultiFileCachingCompiler we load all the cache entries, even for the
    // not-up-to-date file 'top', because we only key off of filename, not off
    // of cache key.
    cacheMatch('Loaded {}/subdir/nested-root.' + extension);
    cacheMatch('Loaded {}/top.' + extension);
    cacheMatch('Loaded {}/yet-another-root.' + extension);
    cacheMatch(`Ran (#1) on: ["/top.${ extension }"]`);
    run.waitSecs(15);
    run.match('Hello world');
    checkCSS(expectedBorderStyles);

    s.write('bad-import.' + extension, importLine('/foo/bad'));
    run.match('Errors prevented startup');
    switch (packageName) {
    case 'less':
      run.match('bad-import.less:1: Unknown import: /foo/bad.less');
      break;
    case 'stylus':
      run.match('bad-import.styl: Stylus compiler error: bad-import.styl:1');
      run.match('failed to locate @import file /foo/bad.styl');
      break;
    }
    run.match('Waiting for file change');

    run.stop();
  });
});

// Tests that rebuilding a compiler plugin re-instantiates the source processor,
// but other changes don't.
selftest.define("compiler plugin caching - local plugin", function () {
  var s = new Sandbox({ fakeMongo: true });

  s.createApp("myapp", "local-compiler-plugin");
  s.cd("myapp");

  var run = startRun(s);

  // The compiler gets used the first time...
  run.match("PrintmeCompiler invocation 1");
  // ... and the program runs the generated code.
  run.match("PMC: Print out bar");
  run.match("PMC: Print out foo");

  s.write("quux.printme", "And print out quux");
  // PrintmeCompiler gets reused.
  run.match("PrintmeCompiler invocation 2");
  // And the right output prints out
  run.match("PMC: Print out bar");
  run.match("PMC: Print out foo");
  run.match("PMC: And print out quux");

  // Restart meteor; see that the disk cache gets used.
  run.stop();
  run = startRun(s);
  // Disk cache gets us up to 3.
  run.match("PrintmeCompiler invocation 3");
  // And the right output prints out
  run.match("PMC: Print out bar");
  run.match("PMC: Print out foo");
  run.match("PMC: And print out quux");

  // Edit the compiler itself.
  s.write('packages/local-plugin/plugin.js',
          s.read('packages/local-plugin/plugin.js').replace(/PMC/, 'pmc'));
  // New PrintmeCompiler object, and empty disk cache dir.
  run.match("PrintmeCompiler invocation 1");
  // And the right output prints out (lower case now)
  run.match("pmc: Print out bar");
  run.match("pmc: Print out foo");
  run.match("pmc: And print out quux");

  run.stop();
});

// Test error on duplicate compiler plugins.
selftest.define("compiler plugins - duplicate extension", () => {
  const s = new Sandbox({ fakeMongo: true });
  s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");

  s.createApp("myapp", "duplicate-compiler-extensions");
  s.cd("myapp");

  let run = startRun(s);
  run.match('Errors prevented startup');
  run.match('conflict: two packages');
  run.match('trying to handle *.myext');

  // Fix it by changing one extension.
  s.write('packages/local-plugin/plugin.js',
          s.read('packages/local-plugin/plugin.js').replace('myext', 'xext'));
  run.match('Modified -- restarting');

  run.stop();
});

// Test error when a source file no longer has an active plugin.
selftest.define("compiler plugins - inactive source", () => {
  const s = new Sandbox({ fakeMongo: true });

  // This app depends on the published package 'glasser:uses-sourcish', and
  // contains a local package 'local-plugin'.
  //
  // glasser:uses-sourcish depends on local-plugin and contains a file
  // 'foo.sourcish'. When glasser:uses-sourcish@0.0.1 was published, a local
  // copy of 'local-plugin' had a plugin which called registerCompiler for the
  // extension '*.sourcish', and so 'foo.sourcish' is in the published isopack
  // as a source file. However, the copy of 'local-plugin' currently in the test
  // app contains no plugins. So we hit this weird error.
  s.createApp('myapp', 'uses-published-package-with-inactive-source');
  s.cd('myapp');

  const run = s.run();
  run.match('myapp');
  run.matchBeforeExit('Started proxy');
  run.match('Errors prevented startup');
  run.match('no plugin found for foo.sourcish in glasser:use-sourcish');
  run.match('none is now');
  run.stop();
});

// Test error when the registerCompiler callback throws.
selftest.define("compiler plugins - compiler throws", () => {
  const s = new Sandbox({ fakeMongo: true });

  s.createApp('myapp', 'compiler-plugin-throws-on-instantiate');
  s.cd('myapp');

  const run = s.run('add', 'local-plugin');
  run.matchErr('Errors while adding packages');
  run.matchErr(
    'While running registerCompiler callback in package local-plugin');
  // XXX This is wrong! The path on disk is packages/local-plugin/plugin.js, but
  // at some point we switched to the servePath which is based on the *plugin*'s
  // "package" name.
  run.matchErr(
    /packages\/compilePrintme_plugin\.js:\d+:\d+: Error in my registerCompiler callback!/
  );
  run.expectExit(1);
});

// Test that compiler plugins can add static assets. Also tests `filenames`
// option to registerCompiler.
selftest.define("compiler plugins - compiler addAsset", () => {
  const s = new Sandbox({ fakeMongo: true });

  s.createApp('myapp', 'compiler-plugin-add-asset');
  s.cd('myapp');

  const run = startRun(s);
  // Test server-side asset.
  run.match("extension is null");  // test getExtension -> null
  run.match("Asset says Print out foo");

  // Test client-side asset.
  const body = getUrl('http://localhost:3000/foo.printme');
  selftest.expectEqual(body, 'Print out foo\n');

  run.stop();
});

// Test that a package can have a single file that is both source code and an
// asset
selftest.define("compiler plugins - addAssets", () => {
  const s = new Sandbox({ fakeMongo: true });

  s.createApp('myapp', 'compiler-plugin-asset-and-source');
  s.cd('myapp');

  const run = startRun(s);

  // Test server-side asset.
  run.match("Printing out my own source code!");

  // Test client-side asset.
  let body = getUrl('http://localhost:3000/packages/' +
    'asset-and-source/asset-and-source.js');
  selftest.expectTrue(body.indexOf('Printing out my own source code!') !== -1);

  // Test that deprecated API still works (added in 1.2.1 in response to people
  // having trouble upgrading to 1.2)
  s.write("packages/asset-and-source/package.js", `Package.describe({
      name: 'asset-and-source',
      version: '0.0.1'
    });

    Package.onUse(function(api) {
      api.addFiles('asset-and-source.js');
      api.addFiles('asset-and-source.js',
        ['client', 'server'], { isAsset: true });
    });
  `);

  // Test server-side asset.
  run.match("Printing out my own source code!");

  // Test client-side asset.
  body = getUrl('http://localhost:3000/packages/' +
    'asset-and-source/asset-and-source.js');
  selftest.expectTrue(body.indexOf('Printing out my own source code!') !== -1);

  // Test error messages for malformed package files
  s.write("packages/asset-and-source/package.js", `Package.describe({
      name: 'asset-and-source',
      version: '0.0.1'
    });

    Package.onUse(function(api) {
      api.addFiles('asset-and-source.js');
      api.addAssets('asset-and-source.js', ['client', 'server']);
      api.addFiles('asset-and-source.js');
    });
  `);

  run.match(/Duplicate source file/);

  s.write("packages/asset-and-source/package.js", `Package.describe({
      name: 'asset-and-source',
      version: '0.0.1'
    });

    Package.onUse(function(api) {
      api.addFiles('asset-and-source.js');
      api.addAssets('asset-and-source.js', ['client', 'server']);
      api.addAssets('asset-and-source.js', ['client', 'server']);
    });
  `);

  run.match(/Duplicate asset file/);

  s.write("packages/asset-and-source/package.js", `Package.describe({
      name: 'asset-and-source',
      version: '0.0.1'
    });

    Package.onUse(function(api) {
      api.addAssets('asset-and-source.js');
    });
  `);

  run.match(/requires a second argument/);

  run.stop();
});


This directory and the files immediately inside it are automatically generated
when you change this package's NPM dependencies. Commit the files in this
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
so that others run the same versions of sub-dependencies.

You should NOT check in the node_modules directory that Meteor automatically
creates; if you are using git, the .gitignore file tells git to ignore it.


// Note that this file is required before we install our Babel hooks in
// ../tool-env/install-babel.js, so we can't use ES2015+ syntax here.

var fs = require("fs");
var path = require("path");

// The dev_bundle/bin command has to come immediately after the meteor
// command, as in `meteor npm` or `meteor node`, because we don't want to
// require("./main.js") for these commands.
var devBundleBinCommand = process.argv[2];
var args = process.argv.slice(3);

function getChildProcess() {
  if (typeof devBundleBinCommand !== "string") {
    return Promise.resolve(null);
  }

  var helpers = require("./dev-bundle-bin-helpers.js");

  return Promise.all([
    helpers.getDevBundle(),
    helpers.getEnv()
  ]).then(function (devBundleAndEnv) {
    var devBundleDir = devBundleAndEnv[0];
    var cmd = helpers.getCommand(devBundleBinCommand, devBundleDir);
    if (! cmd) {
      return null;
    }

    var env = devBundleAndEnv[1];
    var child = require("child_process").spawn(cmd, args, {
      stdio: "inherit",
      env: env
    });

    require("./flush-buffers-on-exit-in-windows.js");

    child.on("error", function (error) {
      console.log(error.stack || error);
    });

    child.on("exit", function (exitCode) {
      process.exit(exitCode);
    });

    return child;
  });
}

module.exports = getChildProcess();


// Test error on duplicate compiler plugins.

selftest.define("compiler plugins - duplicate extension", () => {

const s = new Sandbox({ fakeMongo: true });

keep keep keep keep replace keep keep keep keep

var selftest = require('../tool-testing/selftest.js');
var Sandbox = selftest.Sandbox;
var files = require('../fs/files.js');
import { execFileSync } from '../utils/processes.js';
var _ = require('underscore');

// Copy the contents of one file to another.  In these series of tests, we often
// want to switch contents of package.js files. It is more legible to copy in
// the backup file rather than trying to write into it manually.
//
// XXX: Surely there is a function for this in fs?
// XXX: In which case, perhaps move this to sandbox.
var copyFile = function(from, to, sand) {
  var contents = sand.read(from);
  if (!contents) {
    throw new Error("File " + from + " does not exist.");
  };
  sand.write(to, contents);
};

// Given a sandbox, that has the app as its currend cwd, read the versions file
// and read the plugins list.
//
// sand: a sandbox, that has the main app directory as its cwd.
var getCordovaPluginsList = function(sand) {
  var env = files.currentEnvWithPathsAdded(files.getCurrentNodeBinDir());
  env.METEOR_WAREHOUSE_DIR = sand.warehouse;

  var lines = execFileSync('cordova', ['plugins'],
    {
      cwd: files.pathJoin(sand.cwd, '.meteor', 'local', 'cordova-build'),
      env: env
    }).split("\n");
  if (lines[0].match(/No plugins/)) {
    lines = [];
  }
  lines.sort();
  return lines;
}

// Given a sandbox, that has the app as its currend cwd, read the versions file
// and check that it contains the plugins that we are looking for. We don't
// check the order, we just want to make sure that the right dependencies are
// in.
//
// sand: a sandbox, that has the main app directory as its cwd.
// plugins: an array of plugins in order.
var checkCordovaPlugins = selftest.markStack(function(sand, plugins) {
  var cordovaPlugins = getCordovaPluginsList(sand);

  plugins = _.clone(plugins).sort();

  var i = 0;
  _.each(cordovaPlugins, function(line) {
    if (!line || line === '') {
      return;
    }
    // XXX should check for the version as well?
    selftest.expectEqual(line.split(' ')[0], plugins[i]);
    i++;
  });
  selftest.expectEqual(plugins.length, i);
});

// Like the function above but only looks if a certain plugin is on the list
var checkCordovaPluginExists = selftest.markStack(function(sand, plugin) {
  var cordovaPlugins = getCordovaPluginsList(sand);
  var found = false;
  cordovaPlugins = cordovaPlugins.map(function (line) {
    if (line && line !== '') {
      return line.split(' ')[0];
    }
  });
  selftest.expectTrue(_.contains(cordovaPlugins, plugin));
});

// Given a sandbox, that has the app as its cwd, read the cordova plugins
// file and check that it contains exactly the plugins specified, in order.
//
// sand: a sandbox, that has the main app directory as its cwd.
// plugins: an array of plugins in order. Plugins can be of the form:
//
//    standard-app-plugins (ie: name), in which case this will match any
//    version of that plugin as long as it is included.
//
//    awesome-plugin@1.0.0 (ie: name@version) to match that name at that
//    version explicitly. This is for plugins that we included at a specific
//    version.
var checkUserPlugins = function(sand, plugins) {
  var lines = sand.read(".meteor/cordova-plugins").split("\n");
  var depend = {};
  _.each(lines, function(line) {
    if (!line) {
      return;
    }
    // plugins are stored of the form foo@1.0.0, so this should give us an
    // array [foo, 1.0.0].
    var split = line.split('@');
    var plugins = split[0];
    depend[plugins] = split[1];
  });
  var i = 0;

  _.each(plugins, function (plugins) {
    var split = plugins.split('@');
    if (split.length > 1) {
      selftest.expectEqual(depend[split[0]], split[1]);
    } else {
      var exists = _.has(depend, split[0]);
      selftest.expectEqual(exists, true);
    }
    i++;
  });
  selftest.expectEqual(plugins.length, i);
};

var startAppOnAndroidEmulator = function (s) {
  var run = s.run("run", "android");
  // Building and running the app on the Android Emulator can take a long time.
  run.waitSecs(60);
  run.match("Started app on Android Emulator");
  return run;
}

var addPlatform = function (s, platform) {
  var run = s.run("add-platform", "android");
  // Cordova may need to download cordova-android if it's not already
  // cached (in ~/.cordova).
  run.waitSecs(30);
  run.match("added platform");
}

// Add plugins to an app. Change the contents of the plugins and their
// dependencies, make sure that the app still refreshes.
selftest.define("change cordova plugins", ["cordova"], function () {
  var s = new Sandbox();
  var run;

  s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");

  // Starting a run
  s.createApp("myapp", "package-tests");
  s.cd("myapp");
  run = s.run();
  run.match("myapp");
  run.match("proxy");
  run.waitSecs(30);
  run.match("MongoDB");
  run.match("your app");
  run.match("running at");
  run.match("localhost");

  // Add a local package contains-cordova-plugin.
  s.write(".meteor/packages", "meteor-base \n contains-cordova-plugin");
  run.match("restarted");

  // Change something in the plugin.
  s.cp('packages/contains-cordova-plugin/package2.js', 'packages/contains-cordova-plugin/package.js');
  run.match("restarted");

  // Introduce an error.
  s.cp('packages/contains-cordova-plugin/package3.js', 'packages/contains-cordova-plugin/package.js');
  run.match("valid version");

  // Fix the error.
  s.cp('packages/contains-cordova-plugin/package2.js', 'packages/contains-cordova-plugin/package.js');
  run.match("restarted");
});

// Add plugins through the command line, and make sure that the correct set of
// changes is reflected in .meteor/packages, .meteor/versions and list
selftest.define("add cordova plugins", ["slow", "cordova"], function () {
  var s = new Sandbox();
  var run;

  // Starting a run
  s.createApp("myapp", "package-tests");
  s.cd("myapp");
  s.set("METEOR_OFFLINE_CATALOG", "t");

  run = s.run("remove", "meteor-base");
  run.match("removed");

  run = s.run("run", "android");
  run.matchErr("Please add the Android platform to your project first");
  run.match("meteor add-platform ");

  run = addPlatform(s, 'android');

  run = s.run("add", "cordova:cordova-plugin-camera@1.2.0");
  run.match("Added Cordova plugin cordova-plugin-camera");
  run.expectExit(0);

  run = s.run("add", "cordova:cordova-plugin-file");
  run.matchErr("exact version");
  run.expectExit(1);

  // The current behavior doesn't fail if a plugin is not in the registry until
  // build time.
  run = s.run("add", "cordova:foo@1.0.0");
  run.match("Added Cordova plugin foo");
  run.expectExit(0);

  run = s.run("remove", "cordova:foo");
  run.match("Removed Cordova plugin foo");
  run.expectExit(0);

  checkUserPlugins(s, ["cordova-plugin-camera"]);

  run = s.run("add", "contains-cordova-plugin");
  run.match("added,");
  run.match("contains a cordova plugin");
  run.expectExit(0);

  checkUserPlugins(s, ["cordova-plugin-camera"]);

  run = s.run("list");
  run.match("cordova-plugin-camera");

  run = s.run("list-platforms");
  run.match("android");

  run = s.run("build", '../a', "--server", "localhost:3000");
  run.waitSecs(60);
  run.expectExit(0);

  checkCordovaPlugins(s, ["cordova-plugin-camera",
    "com.phonegap.plugins.facebookconnect"]);

  // Remove a plugin
  run = s.run("remove", "contains-cordova-plugin");
  run.match("removed");

  run = s.run("build", '../a', "--server", "localhost:3000");
  run.waitSecs(60);
  run.expectExit(0);

  run = s.run("remove", "cordova:cordova-plugin-camera");
  run.match("Removed");
  run.expectExit(0);

  run = s.run("build", '../a', "--server", "localhost:3000");
  run.waitSecs(60);
  run.expectExit(0);

  checkCordovaPlugins(s, []);

  run = s.run("add", "cordova:cordova-plugin-device@1.0.1");
  run.match("Added");
  run.expectExit(0);

  run = s.run("build", '../a', "--server", "localhost:3000");
  run.waitSecs(60);
  run.expectExit(0);
  checkCordovaPlugins(s, ["cordova-plugin-device"]);

  run = s.run("remove", "cordova:cordova-plugin-device");
  run.match("Removed");
  run.expectExit(0);

  run = s.run("add", "cordova:com.example.plugin@file://");
  run.matchErr("exact version");
  run.expectExit(1);

  run = s.run("add", "cordova:com.example.plugin@file://../../plugin_directory");
  run.match("Added Cordova plugin com.example.plugin");
  run.expectExit(0);

  checkUserPlugins(s, ["com.example.plugin"]);

  // This should fail beacuse the plugin does not exists at the specified path
  run = s.run("build", '../a', "--server", "localhost:3000");
  run.waitSecs(30);
  run.expectExit(1);

  checkCordovaPlugins(s, []);

  // Add a package with Cordova.depends with local plugin (added from path)
  run = s.run("add", "empty-cordova-plugin");
  run.match("added,");
  run.match("contains an empty cordova plugin");
  run.expectExit(0);
});

selftest.define("remove cordova plugins", ['cordova'], function () {
  var s = new Sandbox();
  var run;

  s.createApp("myapp", "package-tests");
  s.cd("myapp");
  run = s.run("add", "cordova:cordova-plugin-camera@0.3.0");
  run.expectExit(0);

  checkUserPlugins(s, ["cordova-plugin-camera"]);

  // Removing a plugin that hasn't been added should say that it isn't
  // in this project.
  run = s.run("remove", "cordova:blahblah");
  run.matchErr("not in this project");
  run.forbidAll("Removed");
  run.expectExit(1);

  run = s.run("remove", "cordova:blahblah",
              "cordova:cordova-plugin-camera");
  run.matchErr("not in this project");
  run.match("Removed");
  run.expectExit(1);
  checkUserPlugins(s, []);

  run = s.run("add", "cordova:com.example.plugin@file://../../plugin_directory");
  run.match("Added Cordova plugin com.example.plugin");
  run.expectExit(0);
  checkUserPlugins(s, ["com.example.plugin"]);

  run = s.run("remove", "cordova:com.example.plugin");
  run.match("Removed");
  run.expectExit(0);
  checkUserPlugins(s, []);

});

selftest.define("meteor exits when cordova platforms it is currently running \
are removed", ["slow", "cordova"], function () {
  var s = new Sandbox();
  var run;

  s.createApp("myapp", "package-tests");
  s.cd("myapp");

  addPlatform(s, "android");

  run = startAppOnAndroidEmulator(s);

  // Remove a platform via command line
  platformRun = s.run("remove-platform", "android");
  platformRun.match("removed platform");

  run.waitSecs(60);
  run.matchErr("Your app's platforms have changed");
  run.matchErr("Restart meteor");
  run.expectExit(254);

  addPlatform(s, "android");

  // Remove a platform in .meteor/platforms
  run = startAppOnAndroidEmulator(s);

  platforms = s.read(files.pathJoin(".meteor", "platforms"));
  platforms = platforms.replace(/android/g, "");
  s.write(files.pathJoin(".meteor", "platforms"), platforms);

  run.waitSecs(60);
  run.matchErr("Your app's platforms have changed");
  run.matchErr("Restart meteor");
  run.expectExit(254);
});

selftest.define("meteor reinstalls only local cordova plugins on consecutive builds/runs", ["slow", "cordova"], function () {
  var s = new Sandbox();
  var run;

  s.createApp("myapp", "package-tests");
  s.cd("myapp");

  run = addPlatform(s, 'android');

  var
    pluginPath          = '../cordova-local-plugin',
    pluginSource        = "packages/empty-cordova-plugin/plugin",
    androidPluginSource = ".meteor/local/cordova-build/platforms/android/src";

  // Copy fake cordova plugin to ../cordova-local-plugin
  s.mkdir(pluginPath);
  s.cp(pluginSource + '/plugin.xml', pluginPath + '/plugin.xml');
  s.mkdir(pluginPath + '/www');
  s.mkdir(pluginPath + '/src');
  s.mkdir(pluginPath + '/src/android');
  s.cp(pluginSource + '/www/Empty.js', pluginPath +'/www/Empty.js');
  s.cp(
    pluginSource + '/src/android/Empty.java',
    pluginPath + '/src/android/Empty.java'
  );

  // Add the local cordova plugin
  run = s.run("add", "cordova:com.cordova.empty@file://../cordova-local-plugin");
  run.match("Added Cordova plugin com.cordova.empty");
  run.expectExit(0);

  checkUserPlugins(s, ["com.cordova.empty"]);

  // Run meteor and check if the cordova android build have the plugin file.
  run = startAppOnAndroidEmulator(s);
  run.stop();

  selftest.expectTrue(
    s.read(
      androidPluginSource + "/com/cordova/empty/Empty.java"
    ).indexOf('change') === -1
  );
  selftest.expectTrue(
    s.read(
      androidPluginSource + "/com/cordova/empty/Empty.java"
    ).indexOf('CordovaPlugin') > -1
  );

  // Copy changed file to the plugin
  s.cp(
    pluginSource + '/src/android/Empty_changed.java',
    pluginPath + '/src/android/Empty.java'
  );

  // Check if the local plugin will be refreshed
  run = startAppOnAndroidEmulator(s);
  run.stop();

  selftest.expectTrue(
    s.read(
      androidPluginSource + "/com/cordova/empty/Empty.java"
    ).indexOf('change') > -1
  );

  // Now test the same scenario but with builds
  s.cp(
    pluginSource + '/src/android/Empty.java',
    pluginPath + '/src/android/Empty.java'
  );

  run = s.run("build", '../a', "--server", "localhost:3000");
  run.waitSecs(60);
  run.expectExit(0);

  selftest.expectTrue(
    s.read(
      '../a/android/project/src/com/cordova/empty/Empty.java'
    ).indexOf('change') === -1
  );

  checkCordovaPluginExists(s, "com.cordova.empty");

  s.cp(
    pluginSource + '/src/android/Empty_changed.java',
    pluginPath + '/src/android/Empty.java'
  );

  run = s.run("build", '../a', "--server", "localhost:3000");
  run.waitSecs(60);
  run.expectExit(0);

  selftest.expectTrue(
    s.read(
      '../a/android/project/src/com/cordova/empty/Empty.java'
    ).indexOf('change') > -1
  );
});

selftest.define("meteor exits when cordova plugins change", ["slow", "cordova"], function () {
  var s = new Sandbox();
  var run;

  s.createApp("myapp", "package-tests");
  s.cd("myapp");

  addPlatform(s, "android");

  run = startAppOnAndroidEmulator(s);

  // First add a plugin directly.
  var pluginRun = s.run("add", "cordova:cordova-plugin-camera@1.0.0");
  pluginRun.expectExit(0);

  run.waitSecs(60);
  run.matchErr("Your app's Cordova plugins have changed");
  run.matchErr("Restart meteor");
  run.expectExit(254);

  run = startAppOnAndroidEmulator(s);

  // This shouldn't cause an exit because it contains the same plugin
  // that we're already using.
  pluginRun = s.run("add", "contains-old-cordova-plugin");
  pluginRun.expectExit(0);
  run.waitSecs(60);
  run.match("restarted");

  pluginRun = s.run("remove", "contains-old-cordova-plugin");
  pluginRun.expectExit(0);
  run.waitSecs(60);
  run.match("restarted");

  // This exits because it contains a new plugin, facebookconnect.
  pluginRun = s.run("add", "contains-cordova-plugin");
  pluginRun.expectExit(0);
  run.waitSecs(60);
  run.matchErr("Your app's Cordova plugins have changed");
  run.matchErr("Restart meteor");
  run.expectExit(254);

  run = startAppOnAndroidEmulator(s);

  pluginRun = s.run("remove", "contains-cordova-plugin");
  pluginRun.expectExit(0);
  run.waitSecs(60);
  run.matchErr("Your app's Cordova plugins have changed");
  run.matchErr("Restart meteor");
  run.expectExit(254);

  run = startAppOnAndroidEmulator(s);

  pluginRun = s.run("remove", "cordova:cordova-plugin-camera");
  pluginRun.expectExit(0);
  run.waitSecs(60);
  run.matchErr("Your app's Cordova plugins have changed");
  run.matchErr("Restart meteor");
  run.expectExit(254);

  // Adding and removing just a Meteor package that contains plugins
  // should also cause the tool to exit.
  run = startAppOnAndroidEmulator(s);

  pluginRun = s.run("add", "contains-cordova-plugin");
  pluginRun.expectExit(0);
  run.waitSecs(60);
  run.matchErr("Your app's Cordova plugins have changed");
  run.matchErr("Restart meteor");
  run.expectExit(254);

  run = startAppOnAndroidEmulator(s);

  pluginRun = s.run("remove", "contains-cordova-plugin");
  pluginRun.expectExit(0);
  run.waitSecs(60);
  run.matchErr("Your app's Cordova plugins have changed");
  run.matchErr("Restart meteor");
  run.expectExit(254);

  // Adding a package with a newer version of a plugin that we're
  // already using should also cause us to restart.
  pluginRun = s.run("add", "contains-old-cordova-plugin");
  pluginRun.expectExit(0);

  run = startAppOnAndroidEmulator(s);

  pluginRun = s.run("add", "contains-camera-cordova-plugin");
  pluginRun.expectExit(0);

  run.matchErr("Your app's Cordova plugins have changed");
  run.matchErr("Restart meteor");
  run.expectExit(254);
});

var buildAndCheckPluginInStar = selftest.markStack(function (s, name, version) {
  var run = s.run(
    "build", '../a', "--server", "localhost:3000", "--directory");
  run.waitSecs(60);
  run.expectExit(0);

  var starJson = JSON.parse(s.read('../a/bundle/star.json'));
  var program = _.findWhere(starJson.programs, { name: "web.cordova" });
  if (! program) {
    selftest.fail("No cordova program in star.json?");
    return;
  }
  var plugins = program.cordovaDependencies;
  selftest.expectEqual(plugins[name], version);
});

selftest.define("cordova plugins in star.json, direct and transitive", ["slow", "cordova"], function () {
  var s = new Sandbox();
  var run;

  // Starting a run
  s.createApp("myapp", "package-tests");
  s.cd("myapp");
  s.set("METEOR_OFFLINE_CATALOG", "t");

  run = addPlatform(s, 'android');

  // Add a direct dependency: it should appear in star.json after we
  // build.
  run = s.run("add", "cordova:cordova-plugin-camera@1.0.0");
  run.expectExit(0);

  buildAndCheckPluginInStar(s, "cordova-plugin-camera", "1.0.0");

  // Add a Cordova dependency from a package, at a newer version: the
  // plugin should appear in star.json at the version added in the
  // direct dependency, even though it's older than the version that the
  // package uses.
  run = s.run("add", "contains-camera-cordova-plugin");
  run.expectExit(0);

  buildAndCheckPluginInStar(s, "cordova-plugin-camera", "1.0.0");

  // After removing the direct dependency, star.json should contain
  // camera@1.2.0, the version used by the package.
  run = s.run("remove", "cordova:cordova-plugin-camera");
  run.expectExit(0);

  buildAndCheckPluginInStar(s, "cordova-plugin-camera", "1.2.0");

  // If we add another package that uses an older version of the plugin,
  // the version in star.json shouldn't change.
  run = s.run("add", "contains-old-cordova-plugin");
  run.expectExit(0);

  buildAndCheckPluginInStar(s, "cordova-plugin-camera", "1.2.0");

  // If we remove the package that uses a newer version, the version in
  // star.json should change.
  run = s.run("remove", "contains-camera-cordova-plugin");
  run.expectExit(0);

  buildAndCheckPluginInStar(s, "cordova-plugin-camera", "1.0.0");
});