keep keep keep replace keep keep keep keep keep

import _ from 'underscore';
import util from 'util';
import url from 'url';
import path from 'path';
import { Console } from '../console/console.js';
import buildmessage from '../utils/buildmessage.js';
import files from '../fs/files';
import { optimisticReadJsonOrNull } from "../fs/optimistic";
import bundler from '../isobuild/bundler.js';
import archinfo from '../utils/archinfo';
import release from '../packaging/release.js';
import { loadIsopackage } from '../tool-env/isopackets.js';
import utils from '../utils/utils.js';
import XmlBuilder from 'xmlbuilder2';

import { CORDOVA_ARCH, SWIFT_VERSION } from './index.js';

// Hard-coded size constants

const iconsIosSizes = {
  'app_store': '1024x1024',
  'iphone_2x': '120x120',
  'iphone_3x': '180x180',
  'ipad_2x': '152x152',
  'ipad_pro': '167x167',
  'ios_settings_2x': '58x58',
  'ios_settings_3x': '87x87',
  'ios_spotlight_2x': '80x80',
  'ios_spotlight_3x': '120x120',
  'ios_notification_2x': '40x40',
  'ios_notification_3x': '60x60',
  // Legacy
  'ipad': '76x76',
  'ios_settings': '29x29',
  'ios_spotlight': '40x40',
  'ios_notification': '20x20',
  'iphone_legacy': '57x57',
  'iphone_legacy_2x': '114x114',
  'ipad_spotlight_legacy': '50x50',
  'ipad_spotlight_legacy_2x': '100x100',
  'ipad_app_legacy': '72x72',
  'ipad_app_legacy_2x': '144x144'
};

const iconsAndroidSizes = {
  'android_mdpi': '48x48',
  'android_hdpi': '72x72',
  'android_xhdpi': '96x96',
  'android_xxhdpi': '144x144',
  'android_xxxhdpi': '192x192'
};

const launchIosSizes = {
  'iphone5': '640x1136',
  'iphone6': '750x1334',
  'iphone6p_portrait': '1242x2208',
  'iphone6p_landscape': '2208x1242',
  'iphoneX_portrait': '1125x2436',
  'iphoneX_landscape': '2436x1125',
  'ipad_portrait_2x': '1536x2048',
  'ipad_landscape_2x': '2048x1536',
  // Legacy
  'iphone': '320x480',
  'iphone_2x': '640x960',
  'ipad_portrait': '768x1024',
  'ipad_landscape': '1024x768'
};

const launchAndroidSizes = {
  'android_mdpi_portrait': '320x480',
  'android_mdpi_landscape': '480x320',
  'android_hdpi_portrait': '480x800',
  'android_hdpi_landscape': '800x480',
  'android_xhdpi_portrait': '720x1280',
  'android_xhdpi_landscape': '1280x720',
  'android_xxhdpi_portrait': '960x1600',
  'android_xxhdpi_landscape': '1600x960',
  'android_xxxhdpi_portrait': '1280x1920',
  'android_xxxhdpi_landscape': '1920x1280'
};

export class CordovaBuilder {
  constructor(projectContext, projectRoot, options) {
    this.projectContext = projectContext;
    this.projectRoot = projectRoot;
    this.options = options;

    this.resourcesPath = files.pathJoin(
      this.projectRoot,
      'resources');

    this.initalizeDefaults();
  }

  initalizeDefaults() {
    let { cordovaServerPort } = this.options;
    // if --cordova-server-port is not present on run command
    if (!cordovaServerPort) {
      // Convert the appId (a base 36 string) to a number
      const appIdAsNumber = parseInt(this.projectContext.appIdentifier, 36);
      // We use the appId to choose a local server port between 12000-13000.
      // This range should be large enough to avoid collisions with other
      // Meteor apps, and has also been chosen to avoid collisions
      // with other apps or services on the device (although this can never be
      // guaranteed).
      cordovaServerPort = 12000 + (appIdAsNumber % 1000);
    }

    this.metadata = {
      id: 'com.id' + this.projectContext.appIdentifier,
      version: '0.0.1',
      buildNumber: undefined,
      name: files.pathBasename(this.projectContext.projectDir),
      description: 'New Meteor Mobile App',
      author: 'A Meteor Developer',
      email: 'n/a',
      website: 'n/a',
      contentUrl: `http://localhost/`
    };

    // Set some defaults different from the Cordova defaults
    this.additionalConfiguration = {
      global: {
        'webviewbounce': false,
        'DisallowOverscroll': true,
        'WKWebViewOnly': true,
        'SwiftVersion': SWIFT_VERSION
      },
      platform: {
        ios: {},
        android: {
          "AndroidXEnabled": true,
          "AndroidInsecureFileModeEnabled": true
        }
      }
    };

    // Custom elements that will be appended into config.xml's widgets
    this.custom = [];

    // Resource files that will be appended to platform bundle and config.xml
    this.resourceFiles = [];

    const packageMap = this.projectContext.packageMap;

    if (packageMap && packageMap.getInfo('launch-screen')) {
      this.additionalConfiguration.global.AutoHideSplashScreen = false;
      this.additionalConfiguration.global.SplashScreen = 'screen';
      this.additionalConfiguration.global.SplashScreenDelay = 5000;
      this.additionalConfiguration.global.FadeSplashScreenDuration = 250;
      this.additionalConfiguration.global.ShowSplashScreenSpinner = false;
    }

    if (packageMap && packageMap.getInfo('mobile-status-bar')) {
      this.additionalConfiguration.global.StatusBarOverlaysWebView = false;
      this.additionalConfiguration.global.StatusBarStyle = 'default';
    }

    // Default access rules.
    // Rules can be extended with App.accesRule() in mobile-config.js.
    this.accessRules = {
      // Allow the app to ask the system to open these types of URLs.
      // (e.g. in the phone app or an email client)
      'tel:*': { type: 'intent' },
      'geo:*': { type: 'intent' },
      'mailto:*': { type: 'intent' },
      'sms:*': { type: 'intent' },
      'market:*': { type: 'intent' },
      'itms:*': { type: 'intent' },
      'itms-apps:*': { type: 'intent' },

      // Allow navigation to localhost, which is needed for the local server
      'http://localhost': { type: 'navigation' }
    };

    const mobileServerUrl = this.options.mobileServerUrl;
    const serverDomain = mobileServerUrl ?
      utils.parseUrl(mobileServerUrl).hostname : null;

    // If the remote server domain is known, allow access to it for XHR and DDP
    // connections.
    if (serverDomain) {
      // Application Transport Security (new in iOS 9) doesn't allow you
      // to give access to IP addresses (just domains). So we allow access to
      // everything if we don't have a domain, which sets NSAllowsArbitraryLoads.
      if (utils.isIPv4Address(serverDomain)) {
        this.accessRules['*'] = { type: 'network' };
      } else {
        this.accessRules['*://' + serverDomain] = { type: 'network' };

        // Android talks to localhost over 10.0.2.2. This config file is used for
        // multiple platforms, so any time that we say the server is on localhost we
        // should also say it is on 10.0.2.2.
        if (serverDomain === 'localhost') {
          this.accessRules['*://10.0.2.2'] = { type: 'network' };
        }
      }
    }

    this.imagePaths = {
      icon: {},
      splash: {}
    };

    // Defaults are Meteor meatball images located in tools/cordova/assets directory
    const assetsPath = files.pathJoin(__dirname, 'assets');
    const iconsPath = files.pathJoin(assetsPath, 'icons');
    const launchScreensPath = files.pathJoin(assetsPath, 'launchscreens');

    const setDefaultIcon = (size, name) => {
      const imageFile = files.pathJoin(iconsPath, size + '.png');
      if (files.exists(imageFile)) {
        this.imagePaths.icon[name] = imageFile;
      }
    };

    const setDefaultLaunchScreen = (size, name) => {
      const imageFile = files.pathJoin(launchScreensPath, `${size}.png`);
      if (files.exists(imageFile)) {
        this.imagePaths.splash[name] = imageFile;
      }
    };

    _.each(iconsIosSizes, setDefaultIcon);
    _.each(iconsAndroidSizes, setDefaultIcon);
    _.each(launchIosSizes, setDefaultLaunchScreen);
    _.each(launchAndroidSizes, setDefaultLaunchScreen);

    this.pluginsConfiguration = {};
  }

  processControlFile() {
    const controlFilePath =
      files.pathJoin(this.projectContext.projectDir, 'mobile-config.js');

    if (files.exists(controlFilePath)) {
      Console.debug('Processing mobile-config.js');

      buildmessage.enterJob({ title: `processing mobile-config.js` }, () => {
        const code = files.readFile(controlFilePath, 'utf8');

        try {
          files.runJavaScript(code, {
            filename: 'mobile-config.js',
            symbols: { App: createAppConfiguration(this) }
          });
        } catch (error) {
          buildmessage.exception(error);
        }
      });
    }
  }

  writeConfigXmlAndCopyResources(shouldCopyResources = true) {
    let config = XmlBuilder.create({ version: '1.0' }).ele('widget');

    // Set the root attributes
    _.each({
      id: this.metadata.id,
      version: this.metadata.version,
      'android-versionCode': this.metadata.buildNumber,
      'ios-CFBundleVersion': this.metadata.buildNumber,
      xmlns: 'http://www.w3.org/ns/widgets',
      'xmlns:cdv': 'http://cordova.apache.org/ns/1.0',
      'xmlns:android': 'http://schemas.android.com/apk/res/android'
    }, (value, key) => {
      if (value) {
        config.att(key, value);
      }
    });

    // Set the metadata
    config.ele('name').txt(this.metadata.name);
    config.ele('description').txt(this.metadata.description);
    config.ele('author', {
      href: this.metadata.website,
      email: this.metadata.email
    }).txt(this.metadata.author);

    // Set the additional global configuration preferences
    _.each(this.additionalConfiguration.global, (value, key) => {
      config.ele('preference', {
        name: key,
        value: value.toString()
      });
    });

    // Set custom tags into widget element
    _.each(this.custom, elementSet => {
      const tag = config.ele(elementSet);
    });

    config.ele('content', { src: this.metadata.contentUrl });

    // Copy all the access rules
    _.each(this.accessRules, (options, pattern) => {
      const type = options.type;
      options = _.omit(options, 'type');

      if (type === 'intent') {
        config.ele('allow-intent', { href: pattern });
      } else if (type === 'navigation') {
        config.ele('allow-navigation', Object.assign({ href: pattern }, options));
      } else {
        config.ele('access', Object.assign({ origin: pattern }, options));
      }
    });

    const platformElement = {
      ios: config.ele('platform', { name: 'ios' }),
      android: config.ele('platform', { name: 'android' })
    }

    // Set the additional platform-specific configuration preferences
    _.each(this.additionalConfiguration.platform, (prefs, platform) => {
      _.each(prefs, (value, key) => {
        platformElement[platform].ele('preference', {
          name: key,
          value: value.toString()
        });
      });
    });

    // allow http communication
    // TODO: remove it when building for production
    platformElement.android.ele("edit-config")
        .att("file", "app/src/main/AndroidManifest.xml")
        .att("mode", "merge")
        .att("target", "/manifest/application")
          .ele("application")
          .att("android:usesCleartextTraffic", "true");
    if (shouldCopyResources) {
      // Prepare the resources folder
      files.rm_recursive(this.resourcesPath);
      files.mkdir_p(this.resourcesPath);

      Console.debug('Copying resources for mobile apps');

      this.configureAndCopyImages(iconsIosSizes, platformElement.ios, 'icon');
      this.configureAndCopyImages(iconsAndroidSizes, platformElement.android, 'icon');
      this.configureAndCopyImages(launchIosSizes, platformElement.ios, 'splash');
      this.configureAndCopyImages(launchAndroidSizes, platformElement.android, 'splash');
    }

    this.configureAndCopyResourceFiles(
      this.resourceFiles,
      platformElement.ios,
      platformElement.android
    );

    Console.debug('Writing new config.xml');

    const configXmlPath = files.pathJoin(this.projectRoot, 'config.xml');
    const formattedXmlConfig = config.end({ prettyPrint: true });
    files.writeFile(configXmlPath, formattedXmlConfig, 'utf8');
  }

  configureAndCopyImages(sizes, xmlElement, tag) {
    const imageAttributes = (name, width, height, src) => {
      const androidMatch = /android_(.?.dpi)_(landscape|portrait)/g.exec(name);

      let attributes = {
        src: src,
        width: width,
        height: height
      };

      // XXX special case for Android
      if (androidMatch) {
        attributes.density =
          androidMatch[2].substr(0, 4) + '-' + androidMatch[1];
      }

      return attributes;
    };

    _.each(sizes, (size, name) => {
      const [width, height] = size.split('x');

      const suppliedPath = this.imagePaths[tag][name];
      if (!suppliedPath) {
        return;
      }

      const suppliedFilename = _.last(suppliedPath.split(files.pathSep));
      let extension = _.last(suppliedFilename.split('.'));

      // XXX special case for 9-patch png's
      if (suppliedFilename.match(/\.9\.png$/)) {
        extension = '9.png';
      }

      const filename = name + '.' + tag + '.' + extension;
      const src = files.pathJoin('resources', filename);

      // Copy the file to the build folder with a standardized name
      files.copyFile(
        files.pathResolve(this.projectContext.projectDir, suppliedPath),
        files.pathJoin(this.resourcesPath, filename));

      // Set it to the xml tree
      xmlElement.ele(tag, imageAttributes(name, width, height, src));
    });
  }

  configureAndCopyResourceFiles(resourceFiles, iosElement, androidElement) {
    _.each(resourceFiles, resourceFile => {
      // Copy resource files in cordova project root ./resource-files directory keeping original absolute path
      var filepath = files.pathResolve(this.projectContext.projectDir, resourceFile.src);
      files.copyFile(
        filepath,
        files.pathJoin(this.projectRoot, "resource-files", filepath));
      // And entry in config.xml
      if (!resourceFile.platform ||
          (resourceFile.platform && resourceFile.platform === "android")) {
        androidElement.ele('resource-file', {
          src: files.pathJoin("resource-files", filepath),
          target: resourceFile.target
        });
      }
      if (!resourceFile.platform ||
          (resourceFile.platform && resourceFile.platform === "ios")) {
        iosElement.ele('resource-file', {
          src: files.pathJoin("resource-files", filepath),
          target: resourceFile.target
        });
      }
    });
  }

  copyWWW(bundlePath) {
    const wwwPath = files.pathJoin(this.projectRoot, 'www');

    // Remove existing www
    files.rm_recursive(wwwPath);

    // Create www and www/application directories
    const applicationPath = files.pathJoin(wwwPath, 'application');
    files.mkdir_p(applicationPath);

    // Copy Cordova arch program from bundle to www/application
    const programPath = files.pathJoin(bundlePath, 'programs', CORDOVA_ARCH);
    files.cp_r(programPath, applicationPath);

    // Load program.json
    const programJsonPath = files.convertToOSPath(
      files.pathJoin(applicationPath, 'program.json'));
    const program = JSON.parse(files.readFile(programJsonPath, 'utf8'));

    // Load settings
    const settingsFile = this.options.settingsFile;
    const settings = settingsFile ?
      JSON.parse(files.readFile(settingsFile, 'utf8')) : {};
    const publicSettings = settings['public'];

    // Calculate client hash and append to program
    this.appendVersion(program, publicSettings);

    // Write program.json
    files.writeFile(programJsonPath, JSON.stringify(program), 'utf8');

    const bootstrapPage = this.generateBootstrapPage(
      applicationPath, program, publicSettings
    ).await();

    files.writeFile(files.pathJoin(applicationPath, 'index.html'),
      bootstrapPage, 'utf8');
  }

  appendVersion(program, publicSettings) {
    // Note: these version calculations must be kept in agreement with
    // generateClientProgram in packages/webapp/webapp_server.js, or hot
    // code push will reload the app unnecessarily.

    let configDummy = {};
    configDummy.PUBLIC_SETTINGS = publicSettings || {};

    const { WebAppHashing } = loadIsopackage('webapp-hashing');
    const { AUTOUPDATE_VERSION } = process.env;

    program.version = AUTOUPDATE_VERSION ||
      WebAppHashing.calculateClientHash(
        program.manifest, null, configDummy);

    program.versionRefreshable = AUTOUPDATE_VERSION ||
      WebAppHashing.calculateClientHash(
        program.manifest, type => type === "css", configDummy);

    program.versionNonRefreshable = AUTOUPDATE_VERSION ||
      WebAppHashing.calculateClientHash(
        program.manifest, type => type !== "css", configDummy);
  }

  generateBootstrapPage(applicationPath, program, publicSettings) {
    const meteorRelease =
      release.current.isCheckout() ? "none" : release.current.name;

    const manifest = program.manifest;

    const mobileServerUrl = this.options.mobileServerUrl;

    const parsedUrl = url.parse(mobileServerUrl);

    const runtimeConfig = {
      meteorRelease: meteorRelease,
      gitCommitHash: process.env.METEOR_GIT_COMMIT_HASH || files.findGitCommitHash(applicationPath),
      ROOT_URL: mobileServerUrl,
      // XXX propagate it from this.options?
      ROOT_URL_PATH_PREFIX: parsedUrl.pathname.replace(/\/$/,"") || '',
      DDP_DEFAULT_CONNECTION_URL: process.env.DDP_DEFAULT_CONNECTION_URL || mobileServerUrl,
      autoupdate: {
        versions: {
          "web.cordova": {
            version: program.version,
            versionRefreshable: program.versionRefreshable,
            versionNonRefreshable: program.versionNonRefreshable
          }
        }
      },
      appId: this.projectContext.appIdentifier,
      meteorEnv: {
        NODE_ENV: process.env.NODE_ENV || "production",
        TEST_METADATA: process.env.TEST_METADATA || "{}"
      }
    };

    if (publicSettings) {
      runtimeConfig.PUBLIC_SETTINGS = publicSettings;
    }

    const { Boilerplate } = loadIsopackage('boilerplate-generator');

    const boilerplate = new Boilerplate(CORDOVA_ARCH, manifest, {
      urlMapper: _.identity,
      pathMapper: (path) => files.convertToOSPath(
        files.pathJoin(applicationPath, path)),
      baseDataExtension: {
        meteorRuntimeConfig: JSON.stringify(
          encodeURIComponent(JSON.stringify(runtimeConfig)))
      }
    });

    return boilerplate.toHTMLAsync();
  }

  copyBuildOverride() {
    const buildOverridePath =
      files.pathJoin(this.projectContext.projectDir, 'cordova-build-override');

    if (files.exists(buildOverridePath) &&
      files.stat(buildOverridePath).isDirectory()) {
      Console.debug('Copying over the cordova-build-override directory');
      files.cp_r(buildOverridePath, this.projectRoot);
    }
  }
}

function createAppConfiguration(builder) {
  const { settingsFile } = builder.options;
  let settings = null;
  if (settingsFile) {
    settings = optimisticReadJsonOrNull(settingsFile);
    if (!settings) {
      throw new Error("Unreadable --settings file: " + settingsFile);
    }
  }

  /**
   * @namespace App
   * @global
   * @summary The App configuration object in mobile-config.js
   */
  return {
    /**
     * @summary Set your mobile app's core configuration information.
     * @param {Object} options
     * @param {String} [options.id,version,name,description,author,email,website]
     * Each of the options correspond to a key in the app's core configuration
     * as described in the [Cordova documentation](http://cordova.apache.org/docs/en/5.1.1/config_ref_index.md.html#The%20config.xml%20File_core_configuration_elements).
     * @memberOf App
     */
    info: function (options) {
      // check that every key is meaningful
      _.each(options, function (value, key) {
        if (!_.has(builder.metadata, key)) {
          throw new Error("Unknown key in App.info configuration: " + key);
        }
      });

      Object.assign(builder.metadata, options);
    },
    /**
     * @summary Add a preference for your build as described in the
     * [Cordova documentation](http://cordova.apache.org/docs/en/5.1.1/config_ref_index.md.html#The%20config.xml%20File_global_preferences).
     * @param {String} name A preference name supported by Cordova's
     * `config.xml`.
     * @param {String} value The value for that preference.
     * @param {String} [platform] Optional. A platform name (either `ios` or `android`) to add a platform-specific preference.
     * @memberOf App
     */
    setPreference: function (key, value, platform) {
      if (platform) {
        if (!['ios', 'android'].includes(platform)) {
          throw new Error(`Unknown platform in App.setPreference: ${platform}. \
Valid platforms are: ios, android.`);
        }

        builder.additionalConfiguration.platform[platform][key] = value;
      } else {
        builder.additionalConfiguration.global[key] = value;
      }
    },

    /**
     * @summary Like `Meteor.settings`, contains data read from a JSON
     *          file provided via the `--settings` command-line option at
     *          build time, or null if no settings were provided.
     * @memberOf App
     * @type {Object}
     */
    settings,

    /**
     * @summary Set the build-time configuration for a Cordova plugin.
     * @param {String} id The identifier of the plugin you want to
     * configure.
     * @param {Object} config A set of key-value pairs which will be passed
     * at build-time to configure the specified plugin.
     * @memberOf App
     */
    configurePlugin: function (id, config) {
      builder.pluginsConfiguration[id] = config;
    },

    /**
     * @summary Set the icons for your mobile app.
     * @param {Object} icons An Object where the keys are different
     * devices and screen sizes, and values are image paths
     * relative to the project root directory.
     *
     * Valid key values:
     * - `app_store` (1024x1024) // Apple App Store
     * - `iphone_2x` (120x120) // iPhone 5, SE, 6, 6s, 7, 8
     * - `iphone_3x` (180x180) // iPhone 6 Plus, 6s Plus, 7 Plus, 8 Plus, X
     * - `ipad_2x` (152x152) // iPad, iPad mini
     * - `ipad_pro` (167x167) // iPad Pro
     * - `ios_settings_2x` (58x58) // iPhone 5, SE, 6, 6s, 7, 8, iPad, mini, Pro
     * - `ios_settings_3x` (87x87) // iPhone 6 Plus, 6s Plus, 7 Plus, 8 Plus, X
     * - `ios_spotlight_2x` (80x80) // iPhone 5, SE, 6, 6s, 7, 8, iPad, mini, Pro
     * - `ios_spotlight_3x` (120x120) // iPhone 6 Plus, 6s Plus, 7 Plus, 8 Plus, X
     * - `ios_notification_2x` (40x40) // iPhone 5, SE, 6, 6s, 7, 8, iPad, mini, Pro
     * - `ios_notification_3x` (60x60 // iPhone 6 Plus, 6s Plus, 7 Plus, 8 Plus, X
     * - `ipad` (76x76) // Legacy
     * - `ios_settings` (29x29) // Legacy
     * - `ios_spotlight` (40x40) // Legacy
     * - `ios_notification` (20x20) // Legacy
     * - `iphone_legacy` (57x57) // Legacy
     * - `iphone_legacy_2x` (114x114) // Legacy
     * - `ipad_spotlight_legacy` (50x50) // Legacy
     * - `ipad_spotlight_legacy_2x` (100x100) // Legacy
     * - `ipad_app_legacy` (72x72) // Legacy
     * - `ipad_app_legacy_2x` (144x144) // Legacy
     * - `android_mdpi` (48x48)
     * - `android_hdpi` (72x72)
     * - `android_xhdpi` (96x96)
     * - `android_xxhdpi` (144x144)
     * - `android_xxxhdpi` (192x192)
     * @memberOf App
     */
    icons: function (icons) {
      var validDevices =
        Object.keys(iconsIosSizes).concat(Object.keys(iconsAndroidSizes));
      _.each(icons, function (value, key) {
        if (!_.include(validDevices, key)) {
          Console.labelWarn(`${key}: unknown key in App.icons \
configuration. The key may be deprecated.`);
        }
      });
      Object.assign(builder.imagePaths.icon, icons);
    },

    /**
     * @summary Set the launch screen images for your mobile app.
     * @param {Object} launchScreens A dictionary where keys are different
     * devices, screen sizes, and orientations, and the values are image paths
     * relative to the project root directory.
     *
     * For Android, launch screen images should
     * be special "Nine-patch" image files that specify how they should be
     * stretched. See the [Android docs](https://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch).
     *
     * Valid key values:
     * - `iphone5` (640x1136) // iPhone 5, SE
     * - `iphone6` (750x1334) // iPhone 6, 6s, 7, 8
     * - `iphone6p_portrait` (1242x2208) // iPhone 6 Plus, 6s Plus, 7 Plus, 8 Plus
     * - `iphone6p_landscape` (2208x1242) // iPhone 6 Plus, 6s Plus, 7 Plus, 8 Plus
     * - `iphoneX_portrait` (1125x2436) // iPhone X
     * - `iphoneX_landscape` (2436x1125) // iPhone X
     * - `ipad_portrait_2x` (1536x2048) // iPad, iPad mini
     * - `ipad_landscape_2x` (2048x1536) // iPad, iPad mini
     * - `iphone` (320x480) // Legacy
     * - `iphone_2x` (640x960) // Legacy
     * - `ipad_portrait` (768x1024) // Legacy
     * - `ipad_landscape` (1024x768) // Legacy
     * - `android_mdpi_portrait` (320x480)
     * - `android_mdpi_landscape` (480x320)
     * - `android_hdpi_portrait` (480x800)
     * - `android_hdpi_landscape` (800x480)
     * - `android_xhdpi_portrait` (720x1280)
     * - `android_xhdpi_landscape` (1280x720)
     * - `android_xxhdpi_portrait` (960x1600)
     * - `android_xxhdpi_landscape` (1600x960)
     * - `android_xxxhdpi_portrait` (1280x1920)
     * - `android_xxxhdpi_landscape` (1920x1280)
     *
     * @memberOf App
     */
    launchScreens: function (launchScreens) {
      var validDevices =
        Object.keys(launchIosSizes).concat(Object.keys(launchAndroidSizes));

      _.each(launchScreens, function (value, key) {
        if (!_.include(validDevices, key)) {
          Console.labelWarn(`${key}: unknown key in App.launchScreens \
configuration. The key may be deprecated.`);
        }
      });
      Object.assign(builder.imagePaths.splash, launchScreens);
    },

    /**
     * @summary Set a new access rule based on origin domain for your app.
     * By default your application has a limited list of servers it can contact.
     * Use this method to extend this list.
     *
     * Default access rules:
     *
     * - `tel:*`, `geo:*`, `mailto:*`, `sms:*`, `market:*` are allowed and
     *   are handled by the system (e.g. opened in the phone app or an email client)
     * - `http://localhost:*` is used to serve the app's assets from.
     * - The domain or address of the Meteor server to connect to for DDP and
     *   hot code push of new versions.
     *
     * Read more about domain patterns in [Cordova
     * docs](http://cordova.apache.org/docs/en/6.0.0/guide_appdev_whitelist_index.md.html).
     *
     * Starting with Meteor 1.0.4 access rule for all domains and protocols
     * (`<access origin="*"/>`) is no longer set by default due to
     * [certain kind of possible
     * attacks](http://cordova.apache.org/announcements/2014/08/04/android-351.html).
     *
     * @param {String} pattern The pattern defining affected domains or URLs.
     * @param {Object} [options]
     * @param {String} options.type Possible values:
     * - **`'intent'`**: Controls which URLs the app is allowed to ask the system to open.
     *  (e.g. in the phone app or an email client).
     * - **`'navigation'`**: Controls which URLs the WebView itself can be navigated to
     *  (can also needed for iframes).
     * - **`'network'` or undefined**: Controls which network requests (images, XHRs, etc) are allowed to be made.
     * @param {Boolean} options.launchExternal (Deprecated, use `type: 'intent'` instead.)
     * @memberOf App
     */
    accessRule: function (pattern, options) {
      options = options || {};

      if (options.launchExternal) {
        options.type = 'intent';
      }

      builder.accessRules[pattern] = options;
    },

    /**
     * @summary Append custom tags into config's widget element.
     *
     * `App.appendToConfig('<any-xml-content/>');`
     *
     * @param  {String} element The XML you want to include
     * @memberOf App
     */
    appendToConfig: function (xml) {
      builder.custom.push(xml);
    },

    /**
     * @summary Add a resource file for your build as described in the
     * [Cordova documentation](http://cordova.apache.org/docs/en/7.x/config_ref/index.html#resource-file).
     * @param {String} src The project resource path.
     * @param {String} target Resource destination in build.
     * @param {String} [platform] Optional. A platform name (either `ios` or `android`, both if ommited) to add a resource-file entry.
     * @memberOf App
     */
    addResourceFile: function (src, target, platform) {
      builder.resourceFiles.push({
        src: src,
        target: target,
        platform: platform
      });
    }
  };
}


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.


canonicalizeHtml = function(html) {
  var h = html;
  // kill IE-specific comments inserted by DomRange
  h = h.replace(/<!--IE-->/g, '');
  h = h.replace(/<!---->/g, '');
  // ignore exact text of comments
  h = h.replace(/<!--.*?-->/g, '<!---->');
  // make all tags lowercase
  h = h.replace(/<\/?(\w+)/g, function(m) {
    return m.toLowerCase(); });
  // replace whitespace sequences with spaces
  h = h.replace(/\s+/g, ' ');
  // Trim leading and trailing whitespace
  h = h.replace(/^\s+|\s+$/g, '');
  // remove whitespace before and after tags
  h = h.replace(/\s*(<\/?\w.*?>)\s*/g, function (m, tag) {
    return tag; });
  // make tag attributes uniform
  h = h.replace(/<(\w+)\s+(.*?)\s*>/g, function(m, tagName, attrs) {
    // Drop expando property used by Sizzle (part of jQuery) which leaks into
    // attributes in IE8. Note that its value always contains spaces.
    attrs = attrs.replace(/sizcache[0-9]+="[^"]*"/g, ' ');
    // Similarly for expando properties used by jQuery to track data.
    attrs = attrs.replace(/jQuery[0-9]+="[0-9]+"/g, ' ');
    // Similarly for expando properties used to DOMBackend to keep
    // track of callbacks to fire when an element is removed
    attrs = attrs.replace(/\$blaze_teardown_callbacks="[^"]*"/g, ' ');
    // And by DOMRange to keep track of the element's DOMRange
    attrs = attrs.replace(/\$blaze_range="[^"]*"/g, ' ');

    attrs = attrs.replace(/\s*=\s*/g, '=');
    attrs = attrs.replace(/^\s+/g, '');
    attrs = attrs.replace(/\s+$/g, '');
    attrs = attrs.replace(/\s+/g, ' ');
    // quote unquoted attribute values, as in `type=checkbox`.  This
    // will do the wrong thing if there's an `=` in an attribute value.
    attrs = attrs.replace(/(\w)=([^'" >/]+)/g, '$1="$2"');

    // for the purpose of splitting attributes in a string like 'a="b"
    // c="d"', assume they are separated by a single space and values
    // are double- or single-quoted, but allow for spaces inside the
    // quotes.  Split on space following quote.
    var attrList = attrs.replace(/(\w)='([^']*)' /g, "$1='$2'\u0000");
    attrList = attrList.replace(/(\w)="([^"]*)" /g, '$1="$2"\u0000');
    attrList = attrList.split("\u0000");
    // put attributes in alphabetical order
    attrList.sort();

    var tagContents = [tagName];

    for(var i=0; i<attrList.length; i++) {
      // If there were no attrs, attrList could be `[""]`,
      // so skip falsy values.
      if (! attrList[i])
        continue;
      var a = attrList[i].split('=');

      // In IE8, attributes whose value is "" appear
      // without the '=' sign altogether.
      if (a.length < 2)
        a.push("");

      var key = a[0];
      // Drop another expando property used by Sizzle.
      if (key === 'sizset')
        continue;
      var value = a[1];

      // make sure the attribute is doubled-quoted
      if (value.charAt(0) === '"') {
        // Do nothing
      } else {
        if (value.charAt(0) !== "'") {
          // attribute is unquoted. should be unreachable because of
          // regex above.
          value = '"' + value + '"';
        } else {
          // attribute is single-quoted. make it double-quoted.
          value = value.replace(/\"/g, """);
        }
        value = value.replace(/["'`]/g, '"');
      }

      // Encode quotes and double quotes in the attribute.
      var attr = value.slice(1, -1);
      attr = attr.replace(/\"/g, """);
      attr = attr.replace(/\'/g, """);
      value = '"' + attr + '"';

      // Ensure that styles do not end with a semicolon.
      if (key === 'style') {
        value = value.replace(/;\"$/, '"');
      }

      tagContents.push(key+'='+value);
    }
    return '<'+tagContents.join(' ')+'>';
  });
  return h;
};


// we still use a port based on appId on iOS to avoid conflits on local webserver

// we dont need it on android, but the contentUrl can only be one, and we set this

// here to be able to intercept these calls

Package.describe({
  summary: "Serves a Meteor app over HTTP",
  version: '1.12.0'
});

Npm.depends({"basic-auth-connect": "1.0.0",
             "cookie-parser": "1.4.5",
             connect: "3.7.0",
             compression: "1.7.4",
             errorhandler: "1.5.1",
             parseurl: "1.3.3",
             send: "0.17.1",
             "stream-to-string": "1.2.0",
             "qs": "6.10.1",
             useragent: "2.3.0",
             "@vlasky/whomst": "0.1.7"});

Npm.strip({
  multiparty: ["test/"],
  useragent: ["test/"]
});

// whitelist plugin is now included in the core
Cordova.depends({
  'cordova-plugin-meteor-webapp': '2.0.0-beta.0'
});

Package.onUse(function (api) {
  api.use('ecmascript');
  api.use([
    'logging',
    'underscore',
    'routepolicy',
    'modern-browsers',
    'boilerplate-generator',
    'webapp-hashing',
    'inter-process-messaging',
    'callback-hook'
  ], 'server');

  // At response serving time, webapp uses browser-policy if it is loaded. If
  // browser-policy is loaded, then it must be loaded after webapp
  // (browser-policy depends on webapp). So we don't explicitly depend in any
  // way on browser-policy here, but we use it when it is loaded, and it can be
  // loaded after webapp.
  api.mainModule('webapp_server.js', 'server');
  api.export('WebApp', 'server');
  api.export('WebAppInternals', 'server');
  api.export('main', 'server');

  api.mainModule('webapp_client.js', 'client');
  api.export('WebApp', 'client');

  api.mainModule('webapp_cordova.js', 'web.cordova');
});

Package.onTest(function (api) {
  api.use(['tinytest', 'ecmascript', 'webapp', 'http', 'underscore']);
  api.addFiles('webapp_tests.js', 'server');
  api.addFiles('webapp_client_tests.js', 'client');
  api.addFiles('socket_file_tests.js', 'server');

  api.addAssets('modern_test_asset.js', 'web.browser');
  api.addAssets('legacy_test_asset.js', 'legacy');
});


// TODO: remove it when building for production

keep keep keep replace replace replace replace replace replace keep keep

// allow http communication only in development mode

if(process.env.NODE_ENV !== 'production') {


Tinytest.add("oauth - pendingCredential handles Errors", test => {
  const credentialToken = Random.id();

  const testError = new Error("This is a test error");
  testError.stack = 'test stack';
  OAuth._storePendingCredential(credentialToken, testError);

  // Test that the result for the token is the expected error
  const result = OAuth._retrievePendingCredential(credentialToken);
  test.instanceOf(result, Error);
  test.equal(result.message, testError.message);
  test.equal(result.stack, testError.stack);
});

Tinytest.add("oauth - pendingCredential handles Meteor.Errors", test => {
  const credentialToken = Random.id();

  const testError = new Meteor.Error(401, "This is a test error");
  testError.stack = 'test stack';
  OAuth._storePendingCredential(credentialToken, testError);

  // Test that the result for the token is the expected error
  const result = OAuth._retrievePendingCredential(credentialToken);
  test.instanceOf(result, Meteor.Error);
  test.equal(result.error, testError.error);
  test.equal(result.message, testError.message);
  test.equal(result.reason, testError.reason);
  test.equal(result.stack, testError.stack);
  test.isUndefined(result.meteorError);
});

Tinytest.add("oauth - null, undefined key for pendingCredential", test => {
  const cred = Random.id();
  test.throws(() => OAuth._storePendingCredential(null, cred));
  test.throws(() => OAuth._storePendingCredential(undefined, cred));
});

Tinytest.add("oauth - pendingCredential handles duplicate key", test => {
  const key = Random.id();
  const cred = Random.id();
  OAuth._storePendingCredential(key, cred);
  const newCred = Random.id();
  OAuth._storePendingCredential(key, newCred);
  test.equal(OAuth._retrievePendingCredential(key), newCred);
});

Tinytest.add("oauth - pendingCredential requires credential secret", test => {
  const key = Random.id();
  const cred = Random.id();
  const secret = Random.id();
  OAuth._storePendingCredential(key, cred, secret);
  test.equal(OAuth._retrievePendingCredential(key), undefined);
  test.equal(OAuth._retrievePendingCredential(key, secret), cred);
});

Tinytest.add("oauth - _endOfLoginResponse with popup loginStyle supports unspecified ROOT_URL_PATH_PREFIX",
  test => {
    const res = {
      writeHead: () => {},
      end: content => {
        test.matches(
          content,
          /\/packages\/oauth\/end_of_popup_response\.js/
        );
      }
    };
    const details = {
      credentials: {},
      loginStyle: 'popup'
    };
    OAuth._endOfLoginResponse(res, details);
  }
);

Tinytest.add("oauth - _endOfLoginResponse with popup loginStyle supports ROOT_URL_PATH_PREFIX",
  test => {
    const rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
    __meteor_runtime_config__.ROOT_URL_PATH_PREFIX = '/test-root-url-prefix';
    const res = {
      writeHead: () => {},
      end: content => {
        __meteor_runtime_config__.ROOT_URL_PATH_PREFIX = rootUrlPathPrefix;
        test.matches(
          content,
          /\/test-root-url-prefix\/packages\/oauth\/end_of_popup_response\.js/
        );
      }
    };
    const details = {
      credentials: {},
      loginStyle: 'popup'
    };
    OAuth._endOfLoginResponse(res, details);
  }
);

Tinytest.add("oauth - _endOfLoginResponse with redirect loginStyle supports unspecified ROOT_URL_PATH_PREFIX",
  test => {
    const res = {
      writeHead: () => {},
      end: content => {
        test.matches(
          content,
          /\/packages\/oauth\/end_of_redirect_response\.js/
        );
      }
    };
    const details = {
      credentials: {},
      loginStyle: 'redirect',
      query: {
        state: Buffer.from(JSON.stringify({
          redirectUrl: __meteor_runtime_config__.ROOT_URL
        }), 'binary').toString('base64')
      }
    };
    OAuth._endOfLoginResponse(res, details);
  }
);

Tinytest.add("oauth - _endOfLoginResponse with redirect loginStyle supports ROOT_URL_PATH_PREFIX",
  test => {
    const rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
    __meteor_runtime_config__.ROOT_URL_PATH_PREFIX = '/test-root-url-prefix';
    const res = {
      writeHead: () => {},
      end: content => {
        __meteor_runtime_config__.ROOT_URL_PATH_PREFIX = rootUrlPathPrefix;
        test.matches(
          content,
          /\/test-root-url-prefix\/packages\/oauth\/end_of_redirect_response\.js/
        );
      }
    };
    const details = {
      credentials: {},
      loginStyle: 'redirect',
      query: {
        state: Buffer.from(JSON.stringify({
          redirectUrl: __meteor_runtime_config__.ROOT_URL
        }), 'binary').toString('base64')
      }
    };
    OAuth._endOfLoginResponse(res, details);
  }
);


/**
 * @summary Allows for user specified connection options
 * @example http://mongodb.github.io/node-mongodb-native/3.0/reference/connecting/connection-settings/
 * @locus Server
 * @param {Object} options User specified Mongo connection options
 */
Mongo.setConnectionOptions = function setConnectionOptions (options) {
  check(options, Object);
  Mongo._connectionOptions = options;
};


// Each scope gets a bitset that may contain these flags
export const
    SCOPE_TOP = 1,
    SCOPE_FUNCTION = 2,
    SCOPE_VAR = SCOPE_TOP | SCOPE_FUNCTION,
    SCOPE_ASYNC = 4,
    SCOPE_GENERATOR = 8,
    SCOPE_ARROW = 16,
    SCOPE_SIMPLE_CATCH = 32,
    SCOPE_SUPER = 64,
    SCOPE_DIRECT_SUPER = 128

export function functionFlags(async, generator) {
  return SCOPE_FUNCTION | (async ? SCOPE_ASYNC : 0) | (generator ? SCOPE_GENERATOR : 0)
}

// Used in checkLVal and declareName to determine the type of a binding
export const
    BIND_NONE = 0, // Not a binding
    BIND_VAR = 1, // Var-style binding
    BIND_LEXICAL = 2, // Let- or const-style binding
    BIND_FUNCTION = 3, // Function declaration
    BIND_SIMPLE_CATCH = 4, // Simple (identifier pattern) catch binding
    BIND_OUTSIDE = 5 // Special case for function names as bound inside the function


# caching-compiler
[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/caching-compiler) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/caching-compiler)
***

Caching superclasses for Plugin.registerCompiler.
CachingCompiler and MultiFileCachingCompiler are classes designed to be used
with Plugin.registerCompiler. They implement in-memory and on-disk caches for
the files that they process.

CachingCompiler is for compilers whose files can be processed independently of
each other (ie, editing one such file only requires that file to be
rebuilt). MultiFileCachingCompiler is for compilers whose files can depend on
each other, such as CSS preprocessors with `@import` directives.

You should subclass CachingCompiler and define the
following methods: `getCacheKey`, `compileOneFile`, `addCompileResult`, and
`compileResultSize`.

# CompileResult

The data that is cached for each file is of a type that is (implicitly) defined
by your subclass. `caching-compiler` refers to this type as `CompileResult`, but
this isn't a single type: it's up to your subclass to decide what type of data
this is.  You should document what your subclass's `CompileResult` type is.

# Example usage

Your subclass's compiler should call the superclass compiler specifying the
compiler name (used to generate environment variables for debugging and tweaking
in-memory cache size) and the default cache size.

By default, CachingCompiler processes each file in "parallel". That is, if it
needs to yield to read from the disk cache, or if getCacheKey, compileOneFile,
or addCompileResult yields, it will start processing the next few files. To set
how many files can be processed in parallel (including setting it to 1 if your
subclass doesn't support any parallelism), pass the maxParallelism option to the
superclass constructor.

For example (using ES2015 via the ecmascript package):

```js
// CompileResult is a {source, sourceMap} object.
class AwesomeCompiler extends CachingCompiler {
  constructor() {
    super({
      compilerName: 'awesome',
      defaultCacheSize: 1024*1024*10,
    });
  }
  getCacheKey(inputFile) {
    return inputFile.getSourceHash();
  }
  compileResultSize(compileResult) {
    return compileResult.source.length + compileResult.sourceMap.length;
  }
  compileOneFile(inputFile) {
    return Awesomifier.compile(inputFile.getContentsAsString());
  }
  addCompileResult(inputFile, compileResult) {
    inputFile.addJavaScript({
      path: inputFile.getPathInPackage() + '.js',
      sourcePath: inputFile.getPathInPackage(),
      data: compileResult.source,
      sourceMap: compileResult.sourceMap,
    });
  }
}
Plugin.registerCompile({
  extensions: ['awesome'],
}, () => new AwesomeCompiler());
```


/**
 * @summary Generate an absolute URL pointing to the application. The server reads from the `ROOT_URL` environment variable to determine where it is running. This is taken care of automatically for apps deployed to Galaxy, but must be provided when using `meteor build`.
 * @locus Anywhere
 * @param {String} [path] A path to append to the root URL. Do not include a leading "`/`".
 * @param {Object} [options]
 * @param {Boolean} options.secure Create an HTTPS URL.
 * @param {Boolean} options.replaceLocalhost Replace localhost with 127.0.0.1. Useful for services that don't recognize localhost as a domain name.
 * @param {String} options.rootUrl Override the default ROOT_URL from the server environment. For example: "`http://foo.example.com`"
 */
Meteor.absoluteUrl = function (path, options) {
  // path is optional
  if (!options && typeof path === 'object') {
    options = path;
    path = undefined;
  }
  // merge options with defaults
  options = Object.assign({}, Meteor.absoluteUrl.defaultOptions, options || {});

  var url = options.rootUrl;
  if (!url)
    throw new Error("Must pass options.rootUrl or set ROOT_URL in the server environment");

  if (!/^http[s]?:\/\//i.test(url)) // url starts with 'http://' or 'https://'
    url = 'http://' + url; // we will later fix to https if options.secure is set

  if (! url.endsWith("/")) {
    url += "/";
  }

  if (path) {
    // join url and path with a / separator
    while (path.startsWith("/")) {
      path = path.slice(1);
    }
    url += path;
  }

  // turn http to https if secure option is set, and we're not talking
  // to localhost.
  if (options.secure &&
      /^http:/.test(url) && // url starts with 'http:'
      !/http:\/\/localhost[:\/]/.test(url) && // doesn't match localhost
      !/http:\/\/127\.0\.0\.1[:\/]/.test(url)) // or 127.0.0.1
    url = url.replace(/^http:/, 'https:');

  if (options.replaceLocalhost)
    url = url.replace(/^http:\/\/localhost([:\/].*)/, 'http://127.0.0.1$1');

  return url;
};

// allow later packages to override default options
var defaultOptions = Meteor.absoluteUrl.defaultOptions = {};

// available only in a browser environment
var location = typeof window === "object" && window.location;

if (typeof __meteor_runtime_config__ === "object" &&
    __meteor_runtime_config__.ROOT_URL) {
  defaultOptions.rootUrl = __meteor_runtime_config__.ROOT_URL;
} else if (location &&
           location.protocol &&
           location.host) {
  defaultOptions.rootUrl = location.protocol + "//" + location.host;
}

// Make absolute URLs use HTTPS by default if the current window.location
// uses HTTPS. Since this is just a default, it can be overridden by
// passing { secure: false } if necessary.
if (location &&
    location.protocol === "https:") {
  defaultOptions.secure = true;
}

Meteor._relativeToSiteRootUrl = function (link) {
  if (typeof __meteor_runtime_config__ === "object" &&
      link.substr(0, 1) === "/")
    link = (__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "") + link;
  return link;
};


Package.onUse(function(api) {
  api.use('second-imply');
});


keep keep keep keep keep replace keep keep keep keep keep

application-configuration@1.0.0
autopublish@1.0.0
autoupdate@1.0.0
binary-heap@1.0.0
callback-hook@1.0.0
check@1.0.0
ctl-helper@1.0.0
ctl@1.0.0
deps@1.0.0
ejson@1.0.0
follower-livedata@1.0.0
geojson-utils@1.0.0
html-tools@1.0.0
htmljs@1.0.0
id-map@1.0.0
insecure@1.0.0
jquery@1.0.0
json@1.0.0
livedata@1.0.0
logging@1.0.0
meteor@1.0.0
minifiers@1.0.0
minimongo@1.0.0
mongo-livedata@1.0.0
observe-sequence@1.0.0
ordered-dict@1.0.0
random@1.0.0
reactive-dict@1.0.0
reload@1.0.0
retry@1.0.0
routepolicy@1.0.0
session@1.0.0
spacebars-common@1.0.0
spacebars-compiler@1.0.0
spacebars@1.0.0
standard-app-packages@1.0.0
templating@1.0.0
ui@1.0.0
underscore@1.0.0
webapp@1.0.0


Package.describe({
  version: "1.0.0",
  summary: "this program will have dependencies"
});

Package.on_use(function (api) {
});


Circles = new Meteor.Collection("circles");


keep keep keep keep keep replace keep keep keep


Package.describe({
  summary: "contains a plugin",
  version: "1.1.0"
});

Package._transitional_registerBuildPlugin({
  name: "myPlugin",
  use: [],
  sources: [
    'plugin/plugin.js'
  ],
});

Package.on_use(function (api) {
  api.use('accounts-base');
});


keep keep keep replace keep keep keep keep


var path = Npm.require('path');

var doHTMLScanning = function (compileStep, htmlScanner) {

  // XXX use archinfo rather than rolling our own
  if (! compileStep.arch.match(/^browser(\.|$)/))
    // XXX might be nice to throw an error here, but then we'd have to
    // make it so that packages.js ignores html files that appear in
    // the server directories in an app tree.. or, it might be nice to
    // make html files actually work on the server (against jsdom or
    // something)
    return;

  // XXX the way we deal with encodings here is sloppy .. should get
  // religion on that
  var contents = compileStep.read().toString('utf8');
  try {
    var results = htmlScanner.scan(contents, compileStep.inputPath);
  } catch (e) {
    if (e instanceof htmlScanner.ParseError) {
      compileStep.error({
        message: e.message,
        sourcePath: compileStep.inputPath,
        line: e.line
      });
      return;
    } else
      throw e;
  }

  if (results.head)
    compileStep.appendDocument({ section: "head", data: results.head });

  if (results.body)
    compileStep.appendDocument({ section: "body", data: results.body });

  if (results.js) {
    var path_part = path.dirname(compileStep.inputPath);
    if (path_part === '.')
      path_part = '';
    if (path_part.length && path_part !== path.sep)
      path_part = path_part + path.sep;
    var ext = path.extname(compileStep.inputPath);
    var basename = path.basename(compileStep.inputPath, ext);

    // XXX generate a source map

    compileStep.addJavaScript({
      path: path.join(path_part, "template." + basename + ".js"),
      sourcePath: compileStep.inputPath,
      data: results.js
    });
  }
};

Plugin.registerSourceHandler(
  "html", {isTemplate: true},
  function (compileStep) {
    doHTMLScanning(compileStep, html_scanner);
  }
);


NODE_ENV: process.env.NODE_ENV || "development",

TEST_METADATA: process.env.TEST_METADATA || "{}"

meteorEnv = {
  NODE_ENV: process.env.NODE_ENV || "development"
};


# Meteor packages used by this project, one per line.
# Check this file (and the other files in this directory) into your repository.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.

meteor-base             # Packages every Meteor app needs to have
mobile-experience       # Packages for a great mobile UX
mongo                   # The database Meteor supports right now
blaze-html-templates    # Compile .html files into Meteor Blaze views
reactive-var            # Reactive variable for tracker
jquery                  # Helpful client-side library
tracker                 # Meteor's client-side reactive programming library

standard-minifier-css   # CSS minifier run for production mode
standard-minifier-js    # JS minifier run for production mode
es5-shim                # ECMAScript 5 compatibility for older browsers.
ecmascript              # Enable ECMAScript2015+ syntax in app code

autopublish             # Publish all data to the clients (for prototyping)
insecure                # Allow all DB writes from clients (for prototyping)


DiffSequence = {};

// ordered: bool.
// old_results and new_results: collections of documents.
//    if ordered, they are arrays.
//    if unordered, they are IdMaps
DiffSequence.diffQueryChanges = function (ordered, oldResults, newResults,
                                              observer, options) {
  if (ordered)
    DiffSequence.diffQueryOrderedChanges(
      oldResults, newResults, observer, options);
  else
    DiffSequence.diffQueryUnorderedChanges(
      oldResults, newResults, observer, options);
};

DiffSequence.diffQueryUnorderedChanges = function (oldResults, newResults,
                                                       observer, options) {
  options = options || {};
  var projectionFn = options.projectionFn || EJSON.clone;

  if (observer.movedBefore) {
    throw new Error("_diffQueryUnordered called with a movedBefore observer!");
  }

  newResults.forEach(function (newDoc, id) {
    var oldDoc = oldResults.get(id);
    if (oldDoc) {
      if (observer.changed && !EJSON.equals(oldDoc, newDoc)) {
        var projectedNew = projectionFn(newDoc);
        var projectedOld = projectionFn(oldDoc);
        var changedFields =
              DiffSequence.makeChangedFields(projectedNew, projectedOld);
        if (! _.isEmpty(changedFields)) {
          observer.changed(id, changedFields);
        }
      }
    } else if (observer.added) {
      var fields = projectionFn(newDoc);
      delete fields._id;
      observer.added(newDoc._id, fields);
    }
  });

  if (observer.removed) {
    oldResults.forEach(function (oldDoc, id) {
      if (!newResults.has(id))
        observer.removed(id);
    });
  }
};

DiffSequence.diffQueryOrderedChanges = function (old_results, new_results,
                                                     observer, options) {
  options = options || {};
  var projectionFn = options.projectionFn || EJSON.clone;

  var new_presence_of_id = {};
  _.each(new_results, function (doc) {
    if (new_presence_of_id[doc._id])
      Meteor._debug("Duplicate _id in new_results");
    new_presence_of_id[doc._id] = true;
  });

  var old_index_of_id = {};
  _.each(old_results, function (doc, i) {
    if (doc._id in old_index_of_id)
      Meteor._debug("Duplicate _id in old_results");
    old_index_of_id[doc._id] = i;
  });

  // ALGORITHM:
  //
  // To determine which docs should be considered "moved" (and which
  // merely change position because of other docs moving) we run
  // a "longest common subsequence" (LCS) algorithm.  The LCS of the
  // old doc IDs and the new doc IDs gives the docs that should NOT be
  // considered moved.

  // To actually call the appropriate callbacks to get from the old state to the
  // new state:

  // First, we call removed() on all the items that only appear in the old
  // state.

  // Then, once we have the items that should not move, we walk through the new
  // results array group-by-group, where a "group" is a set of items that have
  // moved, anchored on the end by an item that should not move.  One by one, we
  // move each of those elements into place "before" the anchoring end-of-group
  // item, and fire changed events on them if necessary.  Then we fire a changed
  // event on the anchor, and move on to the next group.  There is always at
  // least one group; the last group is anchored by a virtual "null" id at the
  // end.

  // Asymptotically: O(N k) where k is number of ops, or potentially
  // O(N log N) if inner loop of LCS were made to be binary search.

  //////// LCS (longest common sequence, with respect to _id)
  // (see Wikipedia article on Longest Increasing Subsequence,
  // where the LIS is taken of the sequence of old indices of the
  // docs in new_results)
  //
  // unmoved: the output of the algorithm; members of the LCS,
  // in the form of indices into new_results
  var unmoved = [];
  // max_seq_len: length of LCS found so far
  var max_seq_len = 0;
  // seq_ends[i]: the index into new_results of the last doc in a
  // common subsequence of length of i+1 <= max_seq_len
  var N = new_results.length;
  var seq_ends = new Array(N);
  // ptrs:  the common subsequence ending with new_results[n] extends
  // a common subsequence ending with new_results[ptr[n]], unless
  // ptr[n] is -1.
  var ptrs = new Array(N);
  // virtual sequence of old indices of new results
  var old_idx_seq = function(i_new) {
    return old_index_of_id[new_results[i_new]._id];
  };
  // for each item in new_results, use it to extend a common subsequence
  // of length j <= max_seq_len
  for(var i=0; i<N; i++) {
    if (old_index_of_id[new_results[i]._id] !== undefined) {
      var j = max_seq_len;
      // this inner loop would traditionally be a binary search,
      // but scanning backwards we will likely find a subseq to extend
      // pretty soon, bounded for example by the total number of ops.
      // If this were to be changed to a binary search, we'd still want
      // to scan backwards a bit as an optimization.
      while (j > 0) {
        if (old_idx_seq(seq_ends[j-1]) < old_idx_seq(i))
          break;
        j--;
      }

      ptrs[i] = (j === 0 ? -1 : seq_ends[j-1]);
      seq_ends[j] = i;
      if (j+1 > max_seq_len)
        max_seq_len = j+1;
    }
  }

  // pull out the LCS/LIS into unmoved
  var idx = (max_seq_len === 0 ? -1 : seq_ends[max_seq_len-1]);
  while (idx >= 0) {
    unmoved.push(idx);
    idx = ptrs[idx];
  }
  // the unmoved item list is built backwards, so fix that
  unmoved.reverse();

  // the last group is always anchored by the end of the result list, which is
  // an id of "null"
  unmoved.push(new_results.length);

  _.each(old_results, function (doc) {
    if (!new_presence_of_id[doc._id])
      observer.removed && observer.removed(doc._id);
  });
  // for each group of things in the new_results that is anchored by an unmoved
  // element, iterate through the things before it.
  var startOfGroup = 0;
  _.each(unmoved, function (endOfGroup) {
    var groupId = new_results[endOfGroup] ? new_results[endOfGroup]._id : null;
    var oldDoc, newDoc, fields, projectedNew, projectedOld;
    for (var i = startOfGroup; i < endOfGroup; i++) {
      newDoc = new_results[i];
      if (!_.has(old_index_of_id, newDoc._id)) {
        fields = projectionFn(newDoc);
        delete fields._id;
        observer.addedBefore && observer.addedBefore(newDoc._id, fields, groupId);
        observer.added && observer.added(newDoc._id, fields);
      } else {
        // moved
        oldDoc = old_results[old_index_of_id[newDoc._id]];
        projectedNew = projectionFn(newDoc);
        projectedOld = projectionFn(oldDoc);
        fields = DiffSequence.makeChangedFields(projectedNew, projectedOld);
        if (!_.isEmpty(fields)) {
          observer.changed && observer.changed(newDoc._id, fields);
        }
        observer.movedBefore && observer.movedBefore(newDoc._id, groupId);
      }
    }
    if (groupId) {
      newDoc = new_results[endOfGroup];
      oldDoc = old_results[old_index_of_id[newDoc._id]];
      projectedNew = projectionFn(newDoc);
      projectedOld = projectionFn(oldDoc);
      fields = DiffSequence.makeChangedFields(projectedNew, projectedOld);
      if (!_.isEmpty(fields)) {
        observer.changed && observer.changed(newDoc._id, fields);
      }
    }
    startOfGroup = endOfGroup+1;
  });

};

// General helper for diff-ing two objects.
// callbacks is an object like so:
// { leftOnly: function (key, leftValue) {...},
//   rightOnly: function (key, rightValue) {...},
//   both: function (key, leftValue, rightValue) {...},
// }
DiffSequence.diffObjects = function (left, right, callbacks) {
  _.each(left, function (leftValue, key) {
    if (_.has(right, key))
      callbacks.both && callbacks.both(key, leftValue, right[key]);
    else
      callbacks.leftOnly && callbacks.leftOnly(key, leftValue);
  });
  if (callbacks.rightOnly) {
    _.each(right, function(rightValue, key) {
      if (!_.has(left, key))
        callbacks.rightOnly(key, rightValue);
    });
  }
};

DiffSequence.makeChangedFields = function (newDoc, oldDoc) {
  var fields = {};
  DiffSequence.diffObjects(oldDoc, newDoc, {
    leftOnly: function (key, value) {
      fields[key] = undefined;
    },
    rightOnly: function (key, value) {
      fields[key] = value;
    },
    both: function (key, leftValue, rightValue) {
      if (!EJSON.equals(leftValue, rightValue))
        fields[key] = rightValue;
    }
  });
  return fields;
};

DiffSequence.applyChanges = function (doc, changeFields) {
  _.each(changeFields, function (value, key) {
    if (value === undefined)
      delete doc[key];
    else
      doc[key] = value;
  });
};


NODE_ENV: process.env.NODE_ENV || "production",

var TEST_METADATA = process.env.TEST_METADATA || {};

Meteor.isTest = !!TEST_METADATA.isTest;
Meteor.isAppTest = !!TEST_METADATA.isAppTest;

if (Meteor.isClient && (Meteor.isTest || Meteor.isAppTest)) {
  Meteor.startup(function() {
    var testDriverPackage = Package[TEST_METADATA.driverPackage];
    if (!testDriverPackage) {
      throw new Error("Can't find test driver package: " + TEST_METADATA.driverPackage + "}");
    }

    if (!testDriverPackage.runTests) {
      throw new Error("Test driver package " + testDriverPackageName
        + " missing `runTests` export");
    }
    testDriverPackage.runTests();
  });
}


Package.describe({
  summary: "Common code for browser-policy packages",
  version: "1.0.5-beta.12"
});

Package.onUse(function (api) {
  api.use('webapp', 'server');
  api.addFiles('browser-policy-common.js', 'server');
  api.export('BrowserPolicy', 'server');
});


# email

The `email` package allows sending email from a Meteor app.

To see how to use it, read the [`email` section](http://docs.meteor.com/#email) of the Meteor docs.


var TEST_METADATA = process.env.TEST_METADATA || {};

var TEST_METADATA = JSON.parse(TEST_METADATA_STR || "{}");


(function () {

  var config = JSON.parse(document.getElementById("config").innerHTML);

  if (config.setCredentialToken) {
    sessionStorage[config.storagePrefix + config.credentialToken] =
    config.credentialSecret;
  }

  window.location = config.redirectUrl;

})();


Package.describe({
  name: 'debug-only',
  debugOnly: true
});

Package.onUse(function(api) {
  api.addFiles('debug-only.js');
  api.export('Debug');
});


#pragma once

// The following macros define the minimum required platform.  The minimum required platform
// is the earliest version of Windows, Internet Explorer etc. that has the necessary features to run
// your application.  The macros work by enabling all features available on platform versions up to and
// including the version specified.

// Modify the following defines if you have to target a platform prior to the ones specified below.
// Refer to MSDN for the latest info on corresponding values for different platforms.
#ifndef WINVER                  // Specifies that the minimum required platform is Windows 2000.
#define WINVER 0x0500           // Change this to the appropriate value to target other versions of Windows.
#endif

#ifndef _WIN32_WINNT            // Specifies that the minimum required platform is Windows 2000.
#define _WIN32_WINNT 0x0500     // Change this to the appropriate value to target other versions of Windows.
#endif

#ifndef _WIN32_IE               // Specifies that the minimum required platform is Internet Explorer 5.0.
#define _WIN32_IE 0x0500        // Change this to the appropriate value to target other versions of IE.
#endif

#ifndef _WIN32_MSI              // Specifies that the minimum required MSI version is MSI 3.1
#define _WIN32_MSI 310          // Change this to the appropriate value to target other versions of MSI.
#endif


sources = [ "$root_out_dir/libffmpeg.dylib" ]

outputs = [ "$root_out_dir/$chrome_product_full_name.app/Contents/Versions/$chrome_version_full/libffmpeg.dylib" ]

outputs = [ "$root_out_dir/$chrome_product_full_name.app/Contents/Versions/$chrome_version_full//nwjs Framework.framework/libffmpeg.dylib" ]

diff --git a/BUILD.gn b/BUILD.gn
index 0448476773..5274ef931f 100755
--- BUILD.gn
+++ BUILD.gn
@@ -241,7 +241,7 @@ target(link_target_type, "ffmpeg_internal") {
   # Windows builds can't compile without EBP because we can't omit frame
   # pointers like we do on posix.
   if (target_cpu == "x86") {
-    if (using_sanitizer || is_win) {
+    if (true) {
       defines += [ "HAVE_EBP_AVAILABLE=0" ]
     } else {
       defines += [ "HAVE_EBP_AVAILABLE=1" ]
@@ -392,6 +392,12 @@ if (is_component_ffmpeg) {
     # So we can append below and assume they're defined.
     ldflags = []

+    if (is_mac) {
+      ldflags += [
+        "-Wl,-install_name,@loader_path/../libffmpeg.dylib"
+      ]
+    }
+
     if (is_fuchsia || (is_posix && !is_mac)) {
       # Fixes warnings PIC relocation when building as component.
       ldflags += [
@@ -404,12 +410,10 @@ if (is_component_ffmpeg) {
       deps += [ ":ffmpeg_generate_def" ]
       sources = [
         "$target_gen_dir/ffmpeg.def",
+        "../../content/nw/src/ffmpeg.rc",
       ]
     }

-    if (is_mac && !is_component_build) {
-      ldflags += [ "-Wl,-install_name,@rpath/libffmpeg.dylib" ]
-    }
   }
 } else {
   source_set("ffmpeg") {


//
// Remark: Requiring this file will use the "safe" colors API which will not touch String.prototype
//
//   var colors = require('colors/safe);
//   colors.red("foo")
//
//
var colors = require('./lib/colors');
module['exports'] = colors;


// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net_disk_cache_remover.h"

#include "base/bind_helpers.h"
#include "base/synchronization/waitable_event.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "net/disk_cache/disk_cache.h"
#include "net/http/http_cache.h"
#include "net/http/http_transaction_factory.h"
#include "net/url_request/url_request_context_getter.h"
#include "net/url_request/url_request_context.h"
#include "net/base/completion_callback.h"

using content::BrowserThread;
using disk_cache::Backend;
using net::CompletionCallback;
using net::URLRequestContextGetter;

namespace {
// Everything is called and accessed on the IO thread.

void Noop(base::WaitableEvent* completion, int rv) {
  DCHECK(rv == net::OK);
  if (completion)
    completion->Signal();
}

void CallDoomAllEntries(Backend** backend, base::WaitableEvent* completion, int rv) {
  DCHECK(rv == net::OK);
  if (backend && *backend)
    (*backend)->DoomAllEntries(base::Bind(&Noop, completion));
}

void ClearHttpDiskCacheOfContext(URLRequestContextGetter* context_getter,
                                 base::WaitableEvent* completion) {
  typedef Backend* BackendPtr;  // Make line below easier to understand.
  BackendPtr* backend_ptr = new BackendPtr(NULL);
  CompletionCallback callback(base::Bind(&CallDoomAllEntries,
                                         base::Owned(backend_ptr),
                                         completion));

  int rv = context_getter->GetURLRequestContext()->
    http_transaction_factory()->GetCache()->GetBackend(backend_ptr, callback);

  // If not net::ERR_IO_PENDING, then backend pointer is updated but callback
  // is not called, so call it explicitly.
  if (rv != net::ERR_IO_PENDING)
    callback.Run(net::OK);
}

void ClearHttpDiskCacheOnIoThread(
    URLRequestContextGetter* main_context_getter,
    URLRequestContextGetter* media_context_getter,
    base::WaitableEvent* completion1,
    base::WaitableEvent* completion2) {
  ClearHttpDiskCacheOfContext(main_context_getter, completion1);
  ClearHttpDiskCacheOfContext(media_context_getter, completion2);
}

}  // namespace

namespace nw {

void RemoveHttpDiskCache(content::BrowserContext* browser_context,
                        int renderer_child_id) {
  URLRequestContextGetter* main_context_getter =
      browser_context->GetRequestContextForRenderProcess(renderer_child_id);
  URLRequestContextGetter* media_context_getter =
      browser_context->GetMediaRequestContextForRenderProcess(
          renderer_child_id);

  base::WaitableEvent comp1(false, false), comp2(false, false);
  BrowserThread::PostTask(
      BrowserThread::IO, FROM_HERE,
      base::Bind(&ClearHttpDiskCacheOnIoThread,
                 base::Unretained(main_context_getter),
                 base::Unretained(media_context_getter),
                 &comp1, &comp2));
  comp1.Wait();
  comp2.Wait();
}

}  // namespace nw


@@ -241,7 +241,7 @@ target(link_target_type, "ffmpeg_internal") {

#!/usr/bin/env python
import argparse
import getnwisrelease
import getnwversion
import gzip
import os
import platform
import shutil
import sys
import tarfile
import zipfile
from hashlib import sha256

from subprocess import call

steps = ['nw', 'symbol', 'headers', 'others']
################################
# Parse command line args
parser = argparse.ArgumentParser(description='Package nw binaries.')
parser.add_argument('-p','--path', help='Where to find the binaries, like out/Release', required=False)
parser.add_argument('-a','--arch', help='target arch', required=False)
parser.add_argument('-m','--mode', help='package mode', required=False)
parser.add_argument('-i','--icudat', help='icudat override', required=False)
group = parser.add_mutually_exclusive_group()
group.add_argument('-s','--step', choices=steps, help='Execute specified step.', required=False)
group.add_argument('-n','--skip', choices=steps, help='Skip specified step.', required=False)
args = parser.parse_args()

################################
# Init variables.
binaries_location = None        # .../out/Release
platform_name = None            # win/linux/osx
arch = None                     # ia32/x64
step = None                     # nw/chromedriver/symbol
skip = None
nw_ver = None                   # x.xx
dist_dir = None                 # .../out/Release/dist
flavor = args.mode
is_headers_ok = False           # record whether nw-headers generated
package_name = 'nwjs'

if flavor in ['sdk', 'nacl']:
    package_name = 'nwjs-' + args.mode

step = args.step
skip = args.skip
binaries_location = args.path
# If the binaries location is not given, calculate it from script related dir.
if binaries_location == None:
    binaries_location = os.path.join(os.path.dirname(__file__),
            os.pardir, os.pardir, os.pardir, 'out', 'Release')

if not os.path.isabs(binaries_location):
    binaries_location = os.path.join(os.getcwd(), binaries_location)

if not os.path.isdir(binaries_location):
    print 'Invalid path: ' + binaries_location
    exit(-1)
binaries_location = os.path.normpath(binaries_location)
dist_dir = os.path.join(binaries_location, 'dist')

print 'Working on ' + binaries_location

nwfolder = os.path.join(dist_dir, '..', 'nwdist')
try:
    shutil.rmtree(nwfolder)
except:
    pass

if args.icudat != None:
    #FIXME: for some reason they are the same file (hard link) and copy will fail
    os.remove(os.path.join(binaries_location, 'icudtl.dat'))
    shutil.copy(args.icudat, binaries_location)

if sys.platform.startswith('linux'):
    platform_name = 'linux'
elif sys.platform in ('win32', 'cygwin'):
    platform_name = 'win'
elif sys.platform == 'darwin':
    platform_name = 'osx'
else:
    print 'Unsupported platform: ' + sys.platform
    exit(-1)

_arch = platform.architecture()[0]
if _arch == '64bit':
    arch = 'x64'
elif _arch == '32bit':
    arch = 'ia32'
else:
    print 'Unsupported arch: ' + _arch
    exit(-1)

if platform_name == 'win':
    libfile = os.path.join(binaries_location, 'nw.lib')
    libfile2 = os.path.join(binaries_location, 'node.lib')

    shutil.copy(os.path.join(binaries_location, 'nw.dll.lib'), libfile)
    shutil.copy(os.path.join(binaries_location, 'node.dll.lib'), libfile2)

if platform_name == 'win':
    arch = 'ia32'

if platform_name == 'osx':
    # detect output arch
    nw_bin = binaries_location + '/nwjs.app/Contents/MacOS/nwjs'
    import subprocess
    if 'i386' in subprocess.check_output(['file',nw_bin]):
        arch = 'ia32'
    else: # should be 'x86_64'
        arch = 'x64'

if args.arch != None:
    arch = args.arch

nw_ver = getnwversion.nw_version
if getnwisrelease.release == 0:
    nw_ver += getnwisrelease.postfix

################################
# Generate targets
#
# target example:
# {
#    'input'    : [ 'nw', 'nw.pak', ... ]
#    'output'   : 'nwjs-v0.9.2-linux-x64'
#    'compress' : 'tar.gz'
#    'folder'   : True   # Optional. More than 2 files will be put into a seprate folder
#                        # normally, if you want do to this for only 1 file, set this flag.
# }
def generate_target_nw(platform_name, arch, version):
    target = {}
    # Output
    target['output'] = ''.join([
                       package_name, '-',
                       'v', version,
                       '-', platform_name,
                       '-', arch])
    target['keep4test'] = 'nwdist'
    # Compress type
    if platform_name == 'linux':
        target['compress'] = 'tar.gz'
    else:
        target['compress'] = 'zip'
    # Input
    if platform_name == 'linux':
        target['input'] = [
                           'credits.html',
                           'resources.pak',
                           'nw_100_percent.pak',
                           'nw_200_percent.pak',
                           'nw',
                           'icudtl.dat',
                           'locales',
                           'natives_blob.bin',
                           'v8_context_snapshot.bin',
                           'lib/libnw.so',
                           'lib/libnode.so',
                           'lib/libffmpeg.so',
                           'swiftshader/libEGL.so',
                           'swiftshader/libGLESv2.so'
                           ]
        if flavor == 'sdk':
            target['input'].append('nwjc')
            target['input'].append('payload')
            target['input'].append('chromedriver')
            target['input'].append('minidump_stackwalk')
        if flavor in ['nacl','sdk'] :
            target['input'] += ['nacl_helper', 'nacl_helper_bootstrap', 'pnacl']
            if arch == 'x64':
                target['input'].append('nacl_irt_x86_64.nexe')
            else:
                target['input'].append('nacl_irt_x86_32.nexe')

    elif platform_name == 'win':
        target['input'] = [
                           'natives_blob.bin',
                           'v8_context_snapshot.bin',
                           'd3dcompiler_47.dll',
                           'libEGL.dll',
                           'libGLESv2.dll',
                           'nw.dll',
                           'node.dll',
                           'nw_elf.dll',
                           'nw.exe',
                           'locales',
                           'icudtl.dat',
                           'credits.html',
                           'resources.pak',
                           'nw_100_percent.pak',
                           'nw_200_percent.pak',
                           'ffmpeg.dll',
                           'swiftshader\libEGL.dll',
                           'swiftshader\libGLESv2.dll',
                           'notification_helper.exe'
                            # To be removed in CR51
                           ]
        if flavor == 'sdk':
            target['input'].append('nwjc.exe')
            target['input'].append('payload.exe')
            target['input'].append('chromedriver.exe')
        if flavor in ['nacl','sdk'] :
            target['input'].append('pnacl')
            target['input'].append('nacl_irt_x86_64.nexe')
            if arch == 'ia32':
                target['input'].append('nacl_irt_x86_32.nexe')
                target['input'].append('nacl64.exe')
    elif platform_name == 'osx':
        target['input'] = [
                           'nwjs.app',
                           'credits.html',
                          ]
        if flavor == 'sdk':
            target['input'].append('nwjc')
            target['input'].append('payload')
            target['input'].append('chromedriver')
            target['input'].append('minidump_stackwalk')
            target['input'].append('natives_blob.bin')
            target['input'].append('v8_context_snapshot.bin')
    else:
        print 'Unsupported platform: ' + platform_name
        exit(-1)
    return target

def generate_target_chromedriver(platform_name, arch, version):
    if args.mode != 'sdk':
        return generate_target_empty(platform_name, arch, version)

    target = {}
    # Output
    target['output'] = ''.join([
                       'chromedriver-nw-',
                       'v', version,
                       '-', platform_name,
                       '-', arch])
    # Compress type
    if platform_name == 'linux':
        target['compress'] = 'tar.gz'
    else:
        target['compress'] = 'zip'
    # Input
    if platform_name == 'win':
        target['input'] = ['chromedriver.exe']
    else:
        target['input'] = ['chromedriver']
    target['folder'] = True # always create a folder
    return target

def generate_target_symbols(platform_name, arch, version):
    target = {}
    target['output'] = ''.join([package_name, '-symbol-',
                                'v', version,
                                '-', platform_name,
                                '-', arch])
    if platform_name == 'linux':
        target['compress'] = 'tar.gz'
        target['input'] = [
            'nw.sym',
            'libnode.sym',
            'libnw.sym',
            'libffmpeg.sym'
        ]
        if flavor in ['sdk', 'nacl']:
            target['input'].append('nacl_helper.sym')

        target['folder'] = True
    elif platform_name == 'win':
        target['compress'] = None
        target['input'] = ['nw.sym.7z']
        target['output'] = ''.join([package_name, '-symbol-',
                                    'v', version,
                                    '-', platform_name,
                                    '-', arch, '.7z'])
    elif platform_name == 'osx':
        target['compress'] = 'zip'
        target['input'] = [
                          'nwjs.breakpad.tar'
                          ]
        target['folder'] = True
    else:
        print 'Unsupported platform: ' + platform_name
        exit(-1)
    return target

def generate_target_headers(platform_name, arch, version):
    # here, call make_nw_header tool to generate headers
    # then, move to binaries_location
    target = {}
    target['output'] = ''
    target['compress'] = None
    target['input'] = []
    # here , call make-nw-headers.py to generate nw headers
    make_nw_header = os.path.join(os.path.dirname(__file__), \
                                  'make-nw-headers.py')
    print make_nw_header
    res = call(['python', make_nw_header, '-p', binaries_location, '-n', platform_name])
    if res == 0:
        print 'nw-headers generated'
        nw_headers_name = 'nw-headers-v' + version + '.tar.gz'
        nw_headers_path = os.path.join(os.path.dirname(__file__), \
                                       os.pardir, 'tmp', nw_headers_name)
        if os.path.isfile(os.path.join(binaries_location, nw_headers_name)):
            os.remove(os.path.join(binaries_location, nw_headers_name))

        f = open(nw_headers_path, 'rb')
        checksum_file = open(os.path.join(binaries_location, 'SHASUMS256.txt'), 'w')
        with f, checksum_file:
            checksum_file.write('%s %s' % (sha256(f.read()).hexdigest(), nw_headers_name))
        shutil.move(nw_headers_path, binaries_location)
        target['input'].append(nw_headers_name)
        target['input'].append('SHASUMS256.txt')
    else:
        #TODO, handle err
        print 'nw-headers generate failed'
    return target

def generate_target_empty(platform_name, arch, version):
    target = {}
    target['output'] = ''
    target['compress'] = None
    if platform_name == 'win':
        target['input'] = []
    elif platform_name == 'linux' :
        target['input'] = []
    else:
        target['input'] = []
    return target

def generate_target_others(platform_name, arch, version):
    target = {}
    target['output'] = ''
    target['compress'] = None
    if platform_name == 'win':
        target['input'] = ['nw.lib', 'node.lib']
    elif platform_name == 'linux' :
        target['input'] = []
    else:
        target['input'] = []
    return target

################################
# Make packages

#https://gist.github.com/kgn/610907
def ZipDir(inputDir, outputZip):
    '''Zip up a directory and preserve symlinks and empty directories'''
    zipOut = zipfile.ZipFile(outputZip, 'w', compression=zipfile.ZIP_DEFLATED)

    rootLen = len(os.path.dirname(inputDir))
    def _ArchiveDirectory(parentDirectory):
        contents = os.listdir(parentDirectory)
        #store empty directories
        if not contents:
            #http://www.velocityreviews.com/forums/t318840-add-empty-directory-using-zipfile.html
            archiveRoot = parentDirectory[rootLen:].replace('\\', '/').lstrip('/')
            zipInfo = zipfile.ZipInfo(archiveRoot+'/')
            zipOut.writestr(zipInfo, '')
        for item in contents:
            fullPath = os.path.join(parentDirectory, item)
            if os.path.isdir(fullPath) and not os.path.islink(fullPath):
                _ArchiveDirectory(fullPath)
            else:
                archiveRoot = fullPath[rootLen:].replace('\\', '/').lstrip('/')
                if os.path.islink(fullPath):
                    # http://www.mail-archive.com/python-list@python.org/msg34223.html
                    zipInfo = zipfile.ZipInfo(archiveRoot)
                    zipInfo.create_system = 3
                    # long type of hex val of '0xA1ED0000L',
                    # say, symlink attr magic...
                    zipInfo.external_attr = 2716663808L
                    zipOut.writestr(zipInfo, os.readlink(fullPath))
                else:
                    zipOut.write(fullPath, archiveRoot, zipfile.ZIP_DEFLATED)
    _ArchiveDirectory(inputDir)

    zipOut.close()

def compress(from_dir, to_dir, fname, compress):
    from_dir = os.path.normpath(from_dir)
    to_dir = os.path.normpath(to_dir)
    _from = os.path.join(from_dir, fname)
    _to = os.path.join(to_dir, fname)
    if compress == 'zip':
        if os.path.isdir(_from):
            ZipDir(_from, _to + '.zip')
        else:
            z = zipfile.ZipFile(_to + '.zip', 'w', compression=zipfile.ZIP_DEFLATED)
            z.write(_from, fname)
            z.close()
    elif compress == 'tar.gz': # only for folders
        if not os.path.isdir(_from):
            print 'Will not create tar.gz for a single file: ' + _from
            exit(-1)
        with tarfile.open(_to + '.tar.gz', 'w:gz') as tar:
            tar.add(_from, arcname=os.path.basename(_from))
    elif compress == 'gz': # only for single file
        if os.path.isdir(_from):
            print 'Will not create gz for a folder: ' + _from
            exit(-1)
        f_in = open(_from, 'rb')
        f_out = gzip.open(_to + '.gz', 'wb')
        f_out.writelines(f_in)
        f_out.close()
        f_in.close()
    else:
        print 'Unsupported compression format: ' + compress
        exit(-1)

def make_packages(targets):
    # check file existance
    for t in targets:
        for f in t['input']:
            src = os.path.join(binaries_location, f)
            if not os.path.exists(src):
                print 'File does not exist: ', src
                exit(-1)

    # clear the output folder
    if os.path.exists(dist_dir):
        if not os.path.isdir(dist_dir):
            print 'Invalid path: ' + dist_dir
            exit(-1)
        else:
            shutil.rmtree(dist_dir)

    # now let's do it
    os.mkdir(dist_dir)
    for t in targets:
        if len(t['input']) == 0:
            continue
        if t['compress'] == None:
            for f in t['input']:
                src = os.path.join(binaries_location, f)
                if t['output'] != '':
                    dest = os.path.join(dist_dir, t['output'])
                else:
                    dest = os.path.join(dist_dir, f)
                print "Copying " + f
                shutil.copy(src, dest)
        elif (t.has_key('folder') and t['folder'] == True) or len(t['input']) > 1:
            print 'Making "' + t['output'] + '.' + t['compress'] + '"'
            # copy files into a folder then pack
            folder = os.path.join(dist_dir, t['output'])
            os.mkdir(folder)
            if platform_name == 'linux':
                os.mkdir(os.path.join(folder, 'lib'))
            if platform_name in ['linux', 'win']:
                os.mkdir(os.path.join(folder, 'swiftshader'))
            for f in t['input']:
                src = os.path.join(binaries_location, f)
                dest = os.path.join(folder, f)
                if os.path.isdir(src): # like nw.app
                    shutil.copytree(src, dest, symlinks=True)
                else:
                    shutil.copy(src, dest)
            compress(dist_dir, dist_dir, t['output'], t['compress'])
            # remove temp folders
            if (t.has_key('keep4test')) :
                shutil.copytree(folder, nwfolder, symlinks=True)

            shutil.rmtree(folder)
        else:
            # single file
            print 'Making "' + t['output'] + '.' + t['compress'] + '"'
            compress(binaries_location, dist_dir, t['input'][0], t['compress'])

# must be aligned with steps
generators = {}
generators['nw'] = generate_target_nw
generators['chromedriver'] = generate_target_chromedriver
generators['symbol'] = generate_target_symbols
generators['headers'] = generate_target_headers
generators['others'] = generate_target_others
################################
# Process targets
targets = []
for s in steps:
    if (step != None) and (s != step):
            continue
    if (skip != None) and (s == skip):
            continue
    targets.append(generators[s](platform_name, arch, nw_ver))

print 'Creating packages...'
make_packages(targets)

# vim: et:ts=4:sw=4


class DevClass {
  constructor() {
    //console.log("DevClass constructor")
  }
}

var devItem = new DevClass()

console.log(devItem)


# Writing Documents for NW.js {: .doctitle}

[TOC]

## Read Before Writing New Document

Github provides nice representation of Markdown online with its own syntax GFM (Github Flavored Markdown). And many of the developers are reading documents directly on GitHub. To enable the best documentation of NW.js for any developers, **make sure the document is readable on GitHub before submitting your PR**.

The documentation site of NW.js is generated by [MkDocs](http://www.mkdocs.org/) which supports a slightly different Markdown syntax than GFM. So the page may be broken on GitHub while works under MkDocs, or vice versa.
A bad example is not having `.md` suffix in internal links, like `[Build NW.js](Build NW.js)`. On GitHub, the link is broken because the file `Build NW.js` without `.md` does not exist. Always add `.md` suffix to your internal links.

## View the Document Offline
To view the well-formatted documents on your own laptop, you need [Python](https://www.python.org/) and install following dependencies:
```bash
pip install mkdocs pygments pymdown-extensions
```
Run following commands in the root of NW.js repo:
```bash
mkdocs serve
```
Then open your browser and navigate to http://localhost:8000/ .

## Template for API Reference Page

Here is a minimal template for writing a page for NW.js API reference:
```markdown
# Module Name {: .doctitle}

---

[TOC]

Describe the usage and other details of the module here.

## Module.staticMethod(arg1, arg2)

* `arg1` `{String}` description of `arg1`
* `arg2` `{Object}` description of `arg2`
  * `isSet` `{Boolean}` description of the field
* Returns `{Type}` description of the return value

Details about the the method.

## inst.method(arg1, arg2)

## Event: eventName(arg1, arg2)
```

* **[MUST]** use `#` for page title and `##` or `###` headers for sections. **DO NOT** use `=======` or `-------`.
* **[MUST]** add `{: .doctitle}` right to the page title. This is the CSS class name for the document title.
* **[MUST]** add a horizontal ruler with `---` after the page title.
* **[MUST]** add a `[TOC]` after the ruler.
* **[MUST]** quote any code with a back tick (`` ` ``) in the document unless it's in the headers.
* **[MUST]** specify the language for the code blocks.
* Instance methods / properties in headers **[MUST]** use lower case instance name. Bad example: `Window.resizeTo(x,y)`; Good example: `win.resizeTo(x,y)`
* Events **[MUST]** in headers be written as **Event: eventName(args...)**
* **[MUST]** provide detailed list of arguments and return values for methods, including name, type wrapped with brackets ({TypeName}) and whether it's optional in italic (_Optional_).

## Checklist for Pull Request

* [ ] Comply with the [Template for API Reference Page](#template-for-api-reference-page) above
* [ ] Make sure the new page is added to `mkdocs.yml`
* [ ] Make sure the document is readable on GitHub
* [ ] Make sure the links are not broken
* [ ] Add your name and email to [Contributors of Documents.md](Contributors of Documents.md)

## Markdown Extensions
Currently the documentation site of NW.js enables following extensions for generating documents. See `mkdocs.yml` in the root for detailed configuration.

* markdown.extensions.toc
* markdown.extensions.admonition
* markdown.extensions.smarty
* markdown.extensions.nl2br
* markdown.extensions.codehilite
* pymdownx.extra
* pymdownx.inlinehilite
* pymdownx.magiclink
* pymdownx.tilde
* pymdownx.caret
* pymdownx.smartsymbols
* pymdownx.githubemoji
* pymdownx.tasklist
* pymdownx.progressbar
* pymdownx.headeranchor
* pymdownx.arithmatex
* pymdownx.mark
* pymdownx.critic

docs/For Developers/Writing Documents for NW.js.md


import subprocess as sp
import sys
import os
import re

revisions = {
    'nw_src_revision': ''
}

gclient_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))
deps_file = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'DEPS'))

DEFAULT_CONFIG = """solutions = [{"managed": True, "name": "src", "url": "https://github.com/nwjs/chromium.src.git@%(nw_src_revision)s", "custom_deps": {}, "deps_file": "content/nw/DEPS", "safesync_url": "", "custom_vars": {}}]"""

f = open(deps_file)
for line in f:
    if re.match('.*nw_src_revision.*', line):
        revisions['nw_src_revision'] = line.split()[1].strip("',")

cmd = ['gclient', 'sync', '--with_branch_heads', '-f', '--reset', '-v', '--spec', (DEFAULT_CONFIG % revisions)]

print cmd
print gclient_root

os.chdir(gclient_root)
sys.exit(sp.call(cmd, shell=(os.name == 'nt')))


# So we can append below and assume they're defined.

+        "-Wl,-install_name,@loader_path/../libffmpeg.dylib"

if (is_fuchsia || (is_posix && !is_mac)) {

keep keep keep keep keep replace keep keep keep keep

+        "-Wl,-install_name,@loader_path/libffmpeg.dylib"


var argv = require('../')(process.argv.slice(2));
console.dir(argv);


import os
import sys

sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from nw_util import *

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

chrome_options = Options()
testdir = os.path.dirname(os.path.abspath(__file__))
chrome_options.add_argument("nwapp=" + testdir)

driver = webdriver.Chrome(executable_path=os.environ["CHROMEDRIVER"], chrome_options=chrome_options, service_log_path="log", service_args=["--verbose"])
driver.implicitly_wait(2)
try:
    switch_to_app(driver)
    print driver.current_url
    print "wait for devtools open"
    wait_window_handles(driver, 2)
    print driver.window_handles
    print "switch to devtools"
    switch_to_devtools(driver)
    print "click Console panel"
    devtools_click_tab(driver, "console")
    print "check if win.getPrinters receive an array of JSON objects"
    elems = driver.find_elements_by_class_name("console-message-text")
    output = ""
    if len(elems) > 1:
        for i in range(len(elems)):
            if "Array" in elems[i].get_attribute("innerHTML"):
                output = elems[i].get_attribute("innerHTML")
                break
    else:
        output = elems[0].get_attribute("innerHTML")
    print output
    assert("Array" in output)
finally:
    driver.quit()


var fs = require('fs'),
    icons = require('./icons.json'),
    path = require('path');

var IMG_SIZE = 16;

var css = 'td.icon-parent { height: ' + IMG_SIZE + 'px; width: ' + IMG_SIZE + 'px; }\n';
css += 'td.perms {}\n';
css += 'td.file-size { text-align: right; padding-left: 1em; }\n';
css += 'td.display-name { padding-left: 1em; }\n';

Object.keys(icons).forEach(function(key) {
  css += 'i.icon-' + key + ' {\n';
  css += '  display: block; width: 100%; height: 100%; background-repeat: no-repeat;\n';
  css += '  background: url("data:image/png;base64,' + icons[key] + '");\n';
  css += '}\n\n';
});

exports.icons = icons;
exports.css = css;


import time
import os
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from nw_util import *

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
chrome_options = Options()
chrome_options.add_argument("nwapp=" + os.path.dirname(os.path.abspath(__file__)))

driver = webdriver.Chrome(executable_path=os.environ['CHROMEDRIVER'], chrome_options=chrome_options)
time.sleep(1)
try:
    print driver.current_url
    wait_window_handles(driver, 2)
    driver.switch_to_window(driver.window_handles[-1])
    img = driver.find_element_by_id('png')
    assert(img.size['width'] > 50 and img.size['height'] > 50)
    img2 = driver.find_element_by_id('jpg')
    assert(img.size['height'] == img2.size['height'] and img.size['width'] == img2.size['width'])
finally:
    driver.quit()