<commit_before>'use strict'

const {promisify} = require('util')
const {extname, join} = require('path')
const {createWriteStream, mkdir} = require('fs')
const execa = require('execa')
const hasha = require('hasha')
const getStream = require('get-stream')
const {get, omit, pick} = require('lodash')
const contentDisposition = require('content-disposition')
const contentType = require('content-type')
const whichType = require('file-type')
const {is} = require('type-is')
const {extension} = require('mime-types')
const {pipe} = require('mississippi')
const debug = require('debug')('plunger')

const request = require('./util/request')
const {bytesLimit, computeDigest} = require('./util/streams')
const {createTempDirectory} = require('./util/tmpdir')

const {analyzeLocation} = require('./directory')

const mkdirAsync = promisify(mkdir)

async function analyzeURL(location) {
  debug('fetching %s', location)
  const response = await request(location, {
    'If-Modified-Since': 'Fri, 18 Aug 2017 15:25:34 GMT',
    'If-None-Match': '"1503069934-1"'
  })
  const {finalURL, statusCode, redirectURLs} = response
  debug('responded with status %d', statusCode)
  if (redirectURLs.length > 0) {
    debug('redirected %d times: %s', redirectURLs.length, redirectURLs.join(', '))
  }

  const result = {
    headers: extractHeaders(response),
    statusCode,
    redirectURLs,
    originalURL: location,
    finalURL
  }

  if (statusCode === 304) {
    result.type = 'not-modified'
    response.destroy()
    return result
  }

  if (statusCode !== 200) {
    result.type = 'error'
    response.destroy()
    return result
  }

  if (!response.body) {
    result.type = 'no-body'
    response.destroy()
    return result
  }

  result.contentType = extractContentType(response)
  result.contentDisposition = extractContentDisposition(response)

  const fileNameInURL = getFileNameFromLocation(finalURL)
  const fileNameInHeaders = get(result.contentDisposition, 'parameters.filename')

  result.fileName = fileNameInHeaders || fileNameInURL
  result.fileExtension = getExtension(result.fileName)

  if (isXML(response)) {
    result.type = 'xml'
    await attachBody(result, response, 'sha384')
    return result
  }

  if (isHTML(response)) {
    result.type = 'html'
    await attachBody(result, response, 'sha384')
    return result
  }

  if (isPlainText(response)) {
    result.type = 'text'
    await attachBody(result, response, 'sha384')
    return result
  }

  if (isJSON(response)) {
    result.type = 'json'
    await attachBody(result, response, 'sha384')
    return result
  }

  result.fileType = detectFileType(response)

  if (isArchive(result.fileType)) {
    let tmpDirectory
    try {
      result.type = 'archive'
      result.archiveType = result.fileType.ext
      tmpDirectory = await createTempDirectory()
      const filePath = join(tmpDirectory.path, result.fileName)
      const {digest} = await downloadFile(filePath, response.body)
      result.filePath = filePath
      result.tmpDirectory = tmpDirectory.path
      result.digest = digest
      const unarchivedPath = join(tmpDirectory.path, 'unarchived')
      await extractArchive(filePath, unarchivedPath)
      debug('analyzing directory tree')
      result.files = await analyzeLocation(unarchivedPath)
      debug('directory tree analyzed')
    } catch (err) {
      result.checkError = err.message
      response.destroy()
    } finally {
      // tmpDirectory.clean().catch(console.error)
    }
    return result
  }

  // THEN?

  result.type = 'unknown'
  response.destroy()
  return result
}

/* Helpers */

function extractHeaders(response) {
  return omit(response.headers, 'set-cookie', 'connection')
}

function extractContentDisposition(response) {
  const headerValue = response.headers['content-disposition']
  if (!headerValue) return {}
  return contentDisposition.parse(headerValue)
}

function extractContentType(response) {
  const headerValue = response.headers['content-type']
  if (!headerValue) return {}
  const parsedValue = pick(contentType.parse(headerValue), 'type', 'parameters')
  if (parsedValue.type) parsedValue.ext = extension(parsedValue.type)
  return parsedValue
}

function getFileNameFromLocation(location) {
  const pos = location.lastIndexOf('/')
  return pos < location.length ? location.substr(pos + 1) : null
}

function getExtension(fileName) {
  if (!fileName) return null
  return extname(fileName).substr(1).toLowerCase()
}

function detectFileType(response) {
  return whichType(getFirstChunk(response)) || {}
}

function isHTML(response) {
  return is(response, ['html'])
}

function isJSON(response) {
  return is(response, ['json'])
}

function isXML(response) {
  return is(response, ['xml'])
}

function isPlainText(response) {
  return response.headers['content-type'] === 'text/plain'
}

// function isBinary(response) {
//   return [
//     'application/octet-stream',
//     'application/binary',
//   ].includes(response.headers['content-type'])
// }

function isArchive(fileType) {
  if (!fileType) {
    throw new Error('fileType is required')
  }
  return [
    'zip',
    'tar',
    'rar',
    'gz',
    'bz2',
    '7z',
    'xz',
    'exe'
  ].includes(fileType.ext)
}

function getFirstChunk(response) {
  const firstChunk = response.body.read()
  if (!firstChunk) throw new Error('No chunk available')
  response.body.unshift(firstChunk)
  return firstChunk
}

function getDigestString(digest, algorithm = 'sha384') {
  if (!digest) return ''
  return `${algorithm}-${digest}`
}

async function attachBody(result, response, digestAlgorithm = 'sha384') {
  debug('downloading body')
  const buffer = await getStream.buffer(response.body)
  debug('body downloaded')
  result.body = buffer.toString('utf8')
  result.bodyLength = buffer.length
  debug('computing digest')
  const digest = hasha(buffer, {algorithm: digestAlgorithm})
  debug('digest computed')
  result.digest = getDigestString(digest, digestAlgorithm)
}

function downloadFile(path, body) {
  debug('downloading file (w/ on-the-fly digest computation)')
  return new Promise((resolve, reject) => {
    let digest
    let fileSize

    const onDigest = result => {
      digest = getDigestString(result.digest, 'sha384')
      fileSize = result.bodyLength
    }

    pipe(
      body,
      computeDigest('sha384', onDigest),
      bytesLimit(100 * 1024 * 1024),
      createWriteStream(path),
      err => {
        if (err) return reject(err)
        debug('file downloaded')
        resolve({digest, fileSize})
      }
    )
  })
}

async function extractArchive(archivePath, extractPath) {
  await mkdirAsync(extractPath)
  debug('extracting archive')
  await execa('unar', ['-no-directory', '-output-directory', extractPath, archivePath])
  debug('archive extracted')
}

module.exports = {analyzeURL}
<commit_msg>Remove sample/test headers<commit_after>'use strict'

const {promisify} = require('util')
const {extname, join} = require('path')
const {createWriteStream, mkdir} = require('fs')
const execa = require('execa')
const hasha = require('hasha')
const getStream = require('get-stream')
const {get, omit, pick} = require('lodash')
const contentDisposition = require('content-disposition')
const contentType = require('content-type')
const whichType = require('file-type')
const {is} = require('type-is')
const {extension} = require('mime-types')
const {pipe} = require('mississippi')
const debug = require('debug')('plunger')

const request = require('./util/request')
const {bytesLimit, computeDigest} = require('./util/streams')
const {createTempDirectory} = require('./util/tmpdir')

const {analyzeLocation} = require('./directory')

const mkdirAsync = promisify(mkdir)

async function analyzeURL(location) {
  debug('fetching %s', location)
  const response = await request(location)
  const {finalURL, statusCode, redirectURLs} = response
  debug('responded with status %d', statusCode)
  if (redirectURLs.length > 0) {
    debug('redirected %d times: %s', redirectURLs.length, redirectURLs.join(', '))
  }

  const result = {
    headers: extractHeaders(response),
    statusCode,
    redirectURLs,
    originalURL: location,
    finalURL
  }

  if (statusCode === 304) {
    result.type = 'not-modified'
    response.destroy()
    return result
  }

  if (statusCode !== 200) {
    result.type = 'error'
    response.destroy()
    return result
  }

  if (!response.body) {
    result.type = 'no-body'
    response.destroy()
    return result
  }

  result.contentType = extractContentType(response)
  result.contentDisposition = extractContentDisposition(response)

  const fileNameInURL = getFileNameFromLocation(finalURL)
  const fileNameInHeaders = get(result.contentDisposition, 'parameters.filename')

  result.fileName = fileNameInHeaders || fileNameInURL
  result.fileExtension = getExtension(result.fileName)

  if (isXML(response)) {
    result.type = 'xml'
    await attachBody(result, response, 'sha384')
    return result
  }

  if (isHTML(response)) {
    result.type = 'html'
    await attachBody(result, response, 'sha384')
    return result
  }

  if (isPlainText(response)) {
    result.type = 'text'
    await attachBody(result, response, 'sha384')
    return result
  }

  if (isJSON(response)) {
    result.type = 'json'
    await attachBody(result, response, 'sha384')
    return result
  }

  result.fileType = detectFileType(response)

  if (isArchive(result.fileType)) {
    let tmpDirectory
    try {
      result.type = 'archive'
      result.archiveType = result.fileType.ext
      tmpDirectory = await createTempDirectory()
      const filePath = join(tmpDirectory.path, result.fileName)
      const {digest} = await downloadFile(filePath, response.body)
      result.filePath = filePath
      result.tmpDirectory = tmpDirectory.path
      result.digest = digest
      const unarchivedPath = join(tmpDirectory.path, 'unarchived')
      await extractArchive(filePath, unarchivedPath)
      debug('analyzing directory tree')
      result.files = await analyzeLocation(unarchivedPath)
      debug('directory tree analyzed')
    } catch (err) {
      result.checkError = err.message
      response.destroy()
    } finally {
      // tmpDirectory.clean().catch(console.error)
    }
    return result
  }

  // THEN?

  result.type = 'unknown'
  response.destroy()
  return result
}

/* Helpers */

function extractHeaders(response) {
  return omit(response.headers, 'set-cookie', 'connection')
}

function extractContentDisposition(response) {
  const headerValue = response.headers['content-disposition']
  if (!headerValue) return {}
  return contentDisposition.parse(headerValue)
}

function extractContentType(response) {
  const headerValue = response.headers['content-type']
  if (!headerValue) return {}
  const parsedValue = pick(contentType.parse(headerValue), 'type', 'parameters')
  if (parsedValue.type) parsedValue.ext = extension(parsedValue.type)
  return parsedValue
}

function getFileNameFromLocation(location) {
  const pos = location.lastIndexOf('/')
  return pos < location.length ? location.substr(pos + 1) : null
}

function getExtension(fileName) {
  if (!fileName) return null
  return extname(fileName).substr(1).toLowerCase()
}

function detectFileType(response) {
  return whichType(getFirstChunk(response)) || {}
}

function isHTML(response) {
  return is(response, ['html'])
}

function isJSON(response) {
  return is(response, ['json'])
}

function isXML(response) {
  return is(response, ['xml'])
}

function isPlainText(response) {
  return response.headers['content-type'] === 'text/plain'
}

// function isBinary(response) {
//   return [
//     'application/octet-stream',
//     'application/binary',
//   ].includes(response.headers['content-type'])
// }

function isArchive(fileType) {
  if (!fileType) {
    throw new Error('fileType is required')
  }
  return [
    'zip',
    'tar',
    'rar',
    'gz',
    'bz2',
    '7z',
    'xz',
    'exe'
  ].includes(fileType.ext)
}

function getFirstChunk(response) {
  const firstChunk = response.body.read()
  if (!firstChunk) throw new Error('No chunk available')
  response.body.unshift(firstChunk)
  return firstChunk
}

function getDigestString(digest, algorithm = 'sha384') {
  if (!digest) return ''
  return `${algorithm}-${digest}`
}

async function attachBody(result, response, digestAlgorithm = 'sha384') {
  debug('downloading body')
  const buffer = await getStream.buffer(response.body)
  debug('body downloaded')
  result.body = buffer.toString('utf8')
  result.bodyLength = buffer.length
  debug('computing digest')
  const digest = hasha(buffer, {algorithm: digestAlgorithm})
  debug('digest computed')
  result.digest = getDigestString(digest, digestAlgorithm)
}

function downloadFile(path, body) {
  debug('downloading file (w/ on-the-fly digest computation)')
  return new Promise((resolve, reject) => {
    let digest
    let fileSize

    const onDigest = result => {
      digest = getDigestString(result.digest, 'sha384')
      fileSize = result.bodyLength
    }

    pipe(
      body,
      computeDigest('sha384', onDigest),
      bytesLimit(100 * 1024 * 1024),
      createWriteStream(path),
      err => {
        if (err) return reject(err)
        debug('file downloaded')
        resolve({digest, fileSize})
      }
    )
  })
}

async function extractArchive(archivePath, extractPath) {
  await mkdirAsync(extractPath)
  debug('extracting archive')
  await execa('unar', ['-no-directory', '-output-directory', extractPath, archivePath])
  debug('archive extracted')
}

module.exports = {analyzeURL}
<|endoftext|>


<commit_before>// Copyright 2002-2015, University of Colorado Boulder

/**
 * Grunt configuration file for PhET projects.
 * Requires a package.json file containing project settings.
 * Tasks share information via global.phet, see individual tasks for details.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 * @author Jon Olson
 * @author Sam Reid
 * @author John Blanco
 */

// built-in node APIs
var assert = require( 'assert' );
var fs = require( 'fs' );

// 3rd-party packages
/* jshint -W079 */
var _ = require( '../../../sherpa/lodash-2.4.1.min' ); // allow _ to be redefined, contrary to jshintOptions.js
/* jshint +W079 */

// PhET custom grunt tasks
var afterRequirejsBuild = require( '../../../chipper/js/grunt/afterRequirejsBuild' );
var beforeRequirejsBuild = require( '../../../chipper/js/grunt/beforeRequirejsBuild' );
var bumpVersion = require( '../../../chipper/js/grunt/bumpVersion' );
var checkoutShas = require( '../../../chipper/js/grunt/checkoutShas' );
var cloneDependencies = require( '../../../chipper/js/grunt/cloneDependencies' );
var createSim = require( '../../../chipper/js/grunt/createSim' );
var generateLicenseText = require( '../../../chipper/js/grunt/generateLicenseText' );
var generateREADME = require( '../../../chipper/js/grunt/generateREADME' );
var generateThumbnails = require( '../../../chipper/js/grunt/generateThumbnails' );
var pullAll = require( '../../../chipper/js/grunt/pullAll' );
var stringReport = require( '../../../chipper/js/grunt/stringReport' );

//TODO look at why this is necessary
/*
 * In Node, global is the global namespace object.
 * Register fs as a global so it can be accessed through the requirejs build system. Text.js plugin
 * may have a superior way to handle this but I (SR) couldn't get it working after a small amount of effort.
 */
global.fs = fs;

module.exports = function( grunt ) {
  'use strict';

  var FALLBACK_LOCAL = 'en';

  // Read package.json, verify that it contains properties required by all PhET repositories
  assert( fs.existsSync( 'package.json' ), 'repository must have a package.json' );
  var pkg = grunt.file.readJSON( 'package.json' );
  assert( pkg.preload, 'preload missing from package.json' );
  if ( grunt.option( 'together' ) ) {
    console.log( 'Adding additional preload files for together' );
    // add the additional preload files needed to support data exchange using together.js
    pkg.preload.push( '../together/js/together.js' );
    pkg.preload.push( '../together/js/SimIFrameAPI.js' );
    pkg.preload.push( '../together/js/arch.js' );
    pkg.preload.push( '../together/js/datamite.js' );
    pkg.preload.push( '../together/js/api/Common.js' );
    pkg.preload.push( '../together/js/api/' + pkg.name + '-api.js' );
  }
  assert( pkg.name, 'name missing from package.json' );
  assert( pkg.version, 'version missing from package.json' );
  assert( pkg.license, 'license missing from package.json' );

  var globalDefs = {
    // global assertions
    assert: false,
    assertSlow: false,
    // scenery logging
    sceneryLog: false,
    sceneryLayerLog: false,
    sceneryEventLog: false,
    sceneryAccessibilityLog: false,
    phetAllocation: false
  };

  // Project configuration.
  grunt.initConfig( {
    /*
     * Read in the project settings from the package.json file into the pkg property.
     * This allows us to refer to project settings from within this config file.
     */
    pkg: pkg,

    // configure the RequireJS plugin
    requirejs: {

      // builds the minified script
      build: {
        options: {
          almond: true,
          mainConfigFile: 'js/<%= pkg.name %>-config.js',
          out: 'build/<%= pkg.name %>.min.js',
          name: '<%= pkg.name %>-config',

          // Minification strategy.  Put this to none if you want to debug a non-minified but compiled version
          optimize: 'uglify2',
          wrap: true,
//            generateSourceMaps: true, //#42 commented out this line until source maps are fixed
          preserveLicenseComments: false,
          uglify2: {
            output: {
              inline_script: true // escape </script
            },
            compress: {
              global_defs: globalDefs,
              dead_code: true
            }
          },

          //stub out the plugins so their source code won't be included in the minified file
          stubModules: [ 'string', 'audio', 'image' ]
        }
      }
    },

    // configure the JSHint plugin
    jshint: {

      // source files that are specific to this repository
      repoFiles: [ 'js/**/*.js' ],

      // All source files for this repository (repository-specific and dependencies).
      // Excludes kite/js/parser/svgPath.js, which is auto-generated.
      allFiles: [ _.map( pkg.phetLibs, function( repo ) { return '../' + repo + '/js/**/*.js'; } ), '!../kite/js/parser/svgPath.js' ],

      // reference external JSHint options in jshintOptions.js
      options: require( './jshintOptions' )
    }
  } );

  //---------------------------------------------------------------------------------------------------------------
  // Primary tasks
  //---------------------------------------------------------------------------------------------------------------

  // Default task ('grunt')
  grunt.registerTask( 'default', 'Builds the English HTML', [ 'build' ] );

  grunt.registerTask( 'build',
    'Builds the simulation:\n' +
    'with no options, builds HTML for English only\n' +
    '--locales=* : all locales in strings/ directory\n' +
    '--locales=fr : French\n' +
    '--locales=ar,fr,es : Arabic, French and Spanish (comma separated locales)\n' +
    '--localesRepo=$repo : all locales in another repository\'s strings/ directory, ignored if --locales is present\n',
    '--together: adds functionality for data exchange using the together repo',
    [ 'lint-all', 'build-no-lint' ] );

  grunt.registerTask( 'build-no-lint',
    'identical to "build", but does not run "lint-all"',
    [ 'clean', 'generate-license-text', 'before-requirejs-build', 'requirejs:build', 'after-requirejs-build' ] );

  grunt.registerTask( 'lint', 'lint js files that are specific to this repository', [ 'jshint:repoFiles' ] );

  grunt.registerTask( 'lint-all', 'lint all js files that are required to build this repository', [ 'jshint:allFiles' ] );

  grunt.registerTask( 'clean',
    'Erases the build/ directory and all its contents, and recreates the build/ directory',
    function() {
      if ( fs.existsSync( 'build' ) ) {
        grunt.log.writeln( 'Cleaning build directory' );
        grunt.file.delete( 'build' );
      }
      grunt.file.mkdir( 'build' );
    } );

  grunt.registerTask( 'generate-license-text',
    'Generates the license text that will be written to the HTML file',
    function() {
      assert( pkg.preload, 'preload required in package.json' );
      generateLicenseText( grunt, pkg.preload );
    } );

  grunt.registerTask( 'before-requirejs-build',
    '(internal use only) Do things before the requirejs:build task',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.preload, 'preload missing from package.json' );
      beforeRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  grunt.registerTask( 'after-requirejs-build',
    '(internal use only) Do things after the requirejs:build task',
    function() {
      afterRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  //---------------------------------------------------------------------------------------------------------------
  // Utility tasks
  //---------------------------------------------------------------------------------------------------------------

  grunt.registerTask( 'checkout-shas',
    'Check out shas for a project, as specified in dependencies.json',
    function() {
      checkoutShas( grunt, pkg.name, false );
    } );

  grunt.registerTask( 'checkout-master',
    'Check out master branch for all dependencies, as specified in dependencies.json',
    function() {
      checkoutShas( grunt, pkg.name, true );
    } );

  grunt.registerTask( 'create-sim',
    'Creates a sim based on the simula-rasa template.\n' +
    '--name="string" : the repository name\n' +
    '--author="string" : the author name\n' +
    '--title="string" : (optional) the simulation title\n' +
    '--clean=true : (optional) deletes the repository directory if it exists',
    function() {
      createSim( grunt, grunt.option( 'name' ), grunt.option( 'author' ), grunt.option( 'title' ), grunt.option( 'clean' ) );
    } );

  grunt.registerTask( 'generate-published-README',
    'Generates README.md file for a published simulation.',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.simTitleStringKey, 'simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phetLibs, pkg.simTitleStringKey, true /* published */ );
    } );

  grunt.registerTask( 'generate-unpublished-README',
    'Generates README.md file for an unpublished simulation.',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.simTitleStringKey, 'simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phetLibs, pkg.simTitleStringKey, false /* published */ );
    } );

  grunt.registerTask( 'bump-version',
    'This task updates the last value in the version by one. For example from 0.0.0-dev.12 to 0.0.0-dev.13.' +
    'This updates the package.json and js/version.js files, and commits + pushes to git.' +
    'BEWARE: Do not run this task unless your git is clean, otherwise it will commit other work on your repo as well.',
    function() {
      bumpVersion( grunt, pkg.version );
    } );

  grunt.registerTask( 'pull-all',
    'Pull all sibling repositories',
    function() {
      pullAll( grunt );
    } );

  grunt.registerTask( 'string-report',
    'After doing a build, reports on which strings are missing for each locale that was built.',
    function() {
      stringReport( grunt, pkg.name, FALLBACK_LOCAL );
    } );

  //TODO is this vestigial?
  grunt.registerTask( 'clone-dependencies',
    'Clones all dependencies of a project, as listed in package.json phetLibs entry',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      cloneDependencies( grunt, pkg.name, pkg.phetLibs );
    } );

  grunt.registerTask( 'generate-128-thumbnail', 'Generate 128x84 thumbnail', function() {
    generateThumbnails( grunt, pkg.name, 128, 84 );
  } );

  grunt.registerTask( 'generate-600-thumbnail', 'Generate 600x394 thumbnail', function() {
    generateThumbnails( grunt, pkg.name, 600, 394 );
  } );

  grunt.registerTask( 'generate-thumbnails', 'Generate thumbnails', [ 'generate-128-thumbnail', 'generate-600-thumbnail' ] );

  /*
   * Load tasks from grunt plugins that have been installed locally using npm.
   * Put these in package.json and run 'npm install' before running grunt.
   */
  grunt.loadNpmTasks( 'grunt-requirejs' );
  grunt.loadNpmTasks( 'grunt-contrib-jshint' );
};
<commit_msg>improved documentation for --together option, see #130<commit_after>// Copyright 2002-2015, University of Colorado Boulder

/**
 * Grunt configuration file for PhET projects.
 * Requires a package.json file containing project settings.
 * Tasks share information via global.phet, see individual tasks for details.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 * @author Jon Olson
 * @author Sam Reid
 * @author John Blanco
 */

// built-in node APIs
var assert = require( 'assert' );
var fs = require( 'fs' );

// 3rd-party packages
/* jshint -W079 */
var _ = require( '../../../sherpa/lodash-2.4.1.min' ); // allow _ to be redefined, contrary to jshintOptions.js
/* jshint +W079 */

// PhET custom grunt tasks
var afterRequirejsBuild = require( '../../../chipper/js/grunt/afterRequirejsBuild' );
var beforeRequirejsBuild = require( '../../../chipper/js/grunt/beforeRequirejsBuild' );
var bumpVersion = require( '../../../chipper/js/grunt/bumpVersion' );
var checkoutShas = require( '../../../chipper/js/grunt/checkoutShas' );
var cloneDependencies = require( '../../../chipper/js/grunt/cloneDependencies' );
var createSim = require( '../../../chipper/js/grunt/createSim' );
var generateLicenseText = require( '../../../chipper/js/grunt/generateLicenseText' );
var generateREADME = require( '../../../chipper/js/grunt/generateREADME' );
var generateThumbnails = require( '../../../chipper/js/grunt/generateThumbnails' );
var pullAll = require( '../../../chipper/js/grunt/pullAll' );
var stringReport = require( '../../../chipper/js/grunt/stringReport' );

//TODO look at why this is necessary
/*
 * In Node, global is the global namespace object.
 * Register fs as a global so it can be accessed through the requirejs build system. Text.js plugin
 * may have a superior way to handle this but I (SR) couldn't get it working after a small amount of effort.
 */
global.fs = fs;

module.exports = function( grunt ) {
  'use strict';

  var FALLBACK_LOCAL = 'en';

  // Read package.json, verify that it contains properties required by all PhET repositories
  assert( fs.existsSync( 'package.json' ), 'repository must have a package.json' );
  var pkg = grunt.file.readJSON( 'package.json' );
  assert( pkg.preload, 'preload missing from package.json' );
  if ( grunt.option( 'together' ) ) {
    console.log( 'Adding additional preload files for together' );
    // add the additional preload files needed to support data exchange using together.js
    pkg.preload.push( '../together/js/together.js' );
    pkg.preload.push( '../together/js/SimIFrameAPI.js' );
    pkg.preload.push( '../together/js/arch.js' );
    pkg.preload.push( '../together/js/datamite.js' );
    pkg.preload.push( '../together/js/api/Common.js' );
    pkg.preload.push( '../together/js/api/' + pkg.name + '-api.js' );
  }
  assert( pkg.name, 'name missing from package.json' );
  assert( pkg.version, 'version missing from package.json' );
  assert( pkg.license, 'license missing from package.json' );

  var globalDefs = {
    // global assertions
    assert: false,
    assertSlow: false,
    // scenery logging
    sceneryLog: false,
    sceneryLayerLog: false,
    sceneryEventLog: false,
    sceneryAccessibilityLog: false,
    phetAllocation: false
  };

  // Project configuration.
  grunt.initConfig( {
    /*
     * Read in the project settings from the package.json file into the pkg property.
     * This allows us to refer to project settings from within this config file.
     */
    pkg: pkg,

    // configure the RequireJS plugin
    requirejs: {

      // builds the minified script
      build: {
        options: {
          almond: true,
          mainConfigFile: 'js/<%= pkg.name %>-config.js',
          out: 'build/<%= pkg.name %>.min.js',
          name: '<%= pkg.name %>-config',

          // Minification strategy.  Put this to none if you want to debug a non-minified but compiled version
          optimize: 'uglify2',
          wrap: true,
//            generateSourceMaps: true, //#42 commented out this line until source maps are fixed
          preserveLicenseComments: false,
          uglify2: {
            output: {
              inline_script: true // escape </script
            },
            compress: {
              global_defs: globalDefs,
              dead_code: true
            }
          },

          //stub out the plugins so their source code won't be included in the minified file
          stubModules: [ 'string', 'audio', 'image' ]
        }
      }
    },

    // configure the JSHint plugin
    jshint: {

      // source files that are specific to this repository
      repoFiles: [ 'js/**/*.js' ],

      // All source files for this repository (repository-specific and dependencies).
      // Excludes kite/js/parser/svgPath.js, which is auto-generated.
      allFiles: [ _.map( pkg.phetLibs, function( repo ) { return '../' + repo + '/js/**/*.js'; } ), '!../kite/js/parser/svgPath.js' ],

      // reference external JSHint options in jshintOptions.js
      options: require( './jshintOptions' )
    }
  } );

  //---------------------------------------------------------------------------------------------------------------
  // Primary tasks
  //---------------------------------------------------------------------------------------------------------------

  // Default task ('grunt')
  grunt.registerTask( 'default', 'Builds the English HTML', [ 'build' ] );

  grunt.registerTask( 'build',
    'Builds the simulation:\n' +
    'with no options, builds HTML for English only\n' +
    '--locales=* : all locales in strings/ directory\n' +
    '--locales=fr : French\n' +
    '--locales=ar,fr,es : Arabic, French and Spanish (comma separated locales)\n' +
    '--localesRepo=$repo : all locales in another repository\'s strings/ directory, ignored if --locales is present\n',
    '--together: adds preloads needed for data exchange using the together repo',
    [ 'lint-all', 'build-no-lint' ] );

  grunt.registerTask( 'build-no-lint',
    'identical to "build", but does not run "lint-all"',
    [ 'clean', 'generate-license-text', 'before-requirejs-build', 'requirejs:build', 'after-requirejs-build' ] );

  grunt.registerTask( 'lint', 'lint js files that are specific to this repository', [ 'jshint:repoFiles' ] );

  grunt.registerTask( 'lint-all', 'lint all js files that are required to build this repository', [ 'jshint:allFiles' ] );

  grunt.registerTask( 'clean',
    'Erases the build/ directory and all its contents, and recreates the build/ directory',
    function() {
      if ( fs.existsSync( 'build' ) ) {
        grunt.log.writeln( 'Cleaning build directory' );
        grunt.file.delete( 'build' );
      }
      grunt.file.mkdir( 'build' );
    } );

  grunt.registerTask( 'generate-license-text',
    'Generates the license text that will be written to the HTML file',
    function() {
      assert( pkg.preload, 'preload required in package.json' );
      generateLicenseText( grunt, pkg.preload );
    } );

  grunt.registerTask( 'before-requirejs-build',
    '(internal use only) Do things before the requirejs:build task',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.preload, 'preload missing from package.json' );
      beforeRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  grunt.registerTask( 'after-requirejs-build',
    '(internal use only) Do things after the requirejs:build task',
    function() {
      afterRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  //---------------------------------------------------------------------------------------------------------------
  // Utility tasks
  //---------------------------------------------------------------------------------------------------------------

  grunt.registerTask( 'checkout-shas',
    'Check out shas for a project, as specified in dependencies.json',
    function() {
      checkoutShas( grunt, pkg.name, false );
    } );

  grunt.registerTask( 'checkout-master',
    'Check out master branch for all dependencies, as specified in dependencies.json',
    function() {
      checkoutShas( grunt, pkg.name, true );
    } );

  grunt.registerTask( 'create-sim',
    'Creates a sim based on the simula-rasa template.\n' +
    '--name="string" : the repository name\n' +
    '--author="string" : the author name\n' +
    '--title="string" : (optional) the simulation title\n' +
    '--clean=true : (optional) deletes the repository directory if it exists',
    function() {
      createSim( grunt, grunt.option( 'name' ), grunt.option( 'author' ), grunt.option( 'title' ), grunt.option( 'clean' ) );
    } );

  grunt.registerTask( 'generate-published-README',
    'Generates README.md file for a published simulation.',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.simTitleStringKey, 'simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phetLibs, pkg.simTitleStringKey, true /* published */ );
    } );

  grunt.registerTask( 'generate-unpublished-README',
    'Generates README.md file for an unpublished simulation.',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.simTitleStringKey, 'simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phetLibs, pkg.simTitleStringKey, false /* published */ );
    } );

  grunt.registerTask( 'bump-version',
    'This task updates the last value in the version by one. For example from 0.0.0-dev.12 to 0.0.0-dev.13.' +
    'This updates the package.json and js/version.js files, and commits + pushes to git.' +
    'BEWARE: Do not run this task unless your git is clean, otherwise it will commit other work on your repo as well.',
    function() {
      bumpVersion( grunt, pkg.version );
    } );

  grunt.registerTask( 'pull-all',
    'Pull all sibling repositories',
    function() {
      pullAll( grunt );
    } );

  grunt.registerTask( 'string-report',
    'After doing a build, reports on which strings are missing for each locale that was built.',
    function() {
      stringReport( grunt, pkg.name, FALLBACK_LOCAL );
    } );

  //TODO is this vestigial?
  grunt.registerTask( 'clone-dependencies',
    'Clones all dependencies of a project, as listed in package.json phetLibs entry',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      cloneDependencies( grunt, pkg.name, pkg.phetLibs );
    } );

  grunt.registerTask( 'generate-128-thumbnail', 'Generate 128x84 thumbnail', function() {
    generateThumbnails( grunt, pkg.name, 128, 84 );
  } );

  grunt.registerTask( 'generate-600-thumbnail', 'Generate 600x394 thumbnail', function() {
    generateThumbnails( grunt, pkg.name, 600, 394 );
  } );

  grunt.registerTask( 'generate-thumbnails', 'Generate thumbnails', [ 'generate-128-thumbnail', 'generate-600-thumbnail' ] );

  /*
   * Load tasks from grunt plugins that have been installed locally using npm.
   * Put these in package.json and run 'npm install' before running grunt.
   */
  grunt.loadNpmTasks( 'grunt-requirejs' );
  grunt.loadNpmTasks( 'grunt-contrib-jshint' );
};
<|endoftext|>


<commit_before>var chai = require('chai');
var expect = chai.expect;

var replace = require('../lib/replace.js').replace;

describe('replacer', function() {
    describe('with default args', function() {
        it('should replace numbers in simple number value', function() {
            expect(replace(123456)).to.eq('***456');
            expect(replace('123456')).to.eq('***456');
        });

        it('should replace numbers in float', function() {
            expect(replace(12.345)).to.eq('****45');
            expect(replace('12.345')).to.eq('****45');
        });
    });

    describe('with custom args', function() {
        var curriedReplace = function(value) {
            return replace(value, {replaceCount: 5, replaceSymbol: '!', numberDotSymbol: ','});
        };

        it('should replace numbers in simple number value', function() {
            expect(curriedReplace(123456)).to.eq('!!!!!6');
            expect(curriedReplace('123456')).to.eq('!!!!!6');
        });

        it('should replace numbers in float', function() {
            expect(curriedReplace(12.3456)).to.eq('!!!!!!6');
        });

        it('should replace numbers in string with custom dot', function() {
            expect(curriedReplace('12,3456')).to.eq('!!!!!!6');
        });
    });

    it('should fail', function() {
        var errorMessage = 'Value should be a number';

        expect(replace.bind(null, 'asd')).to.throw(errorMessage);
        expect(replace.bind(null, 'qwe.rty')).to.throw(errorMessage);
        expect(replace.bind(null, '123.456.789')).to.throw(errorMessage);
        expect(replace.bind(null, null)).to.throw(errorMessage);
        expect(replace.bind(null, undefined)).to.throw(errorMessage);
        expect(replace.bind(null, {})).to.throw(errorMessage);
        expect(replace.bind(null, {toString: function() {return '12.345'}})).to.throw(errorMessage);
        expect(replace.bind(null, Infinity)).to.throw(errorMessage);
        expect(replace.bind(null, -Infinity)).to.throw(errorMessage);
        expect(replace.bind(null, NaN)).to.throw(errorMessage);
    });
});
<commit_msg>added new tests<commit_after>var chai = require('chai');
var expect = chai.expect;

var replace = require('../lib/replace.js').replace;

describe('replacer', function() {
    describe('with default args', function() {
        it('should replace numbers in simple number value', function() {
            expect(replace(123456)).to.eq('***456');
            expect(replace('123456')).to.eq('***456');
        });

        it('should replace numbers in float', function() {
            expect(replace(12.345)).to.eq('****45');
            expect(replace('12.345')).to.eq('****45');
        });

        it('should replace all symbols if number length < replace count', function() {
            expect(replace(12)).to.eq('**');
            expect(replace('1.2')).to.eq('***');
            expect(replace(1)).to.eq('*');
            expect(replace('1')).to.eq('*');
        });
    });

    describe('with custom args', function() {
        var curriedReplace = function(value) {
            return replace(value, {replaceCount: 5, replaceSymbol: '!', numberDotSymbol: ','});
        };

        it('should replace numbers in simple number value', function() {
            expect(curriedReplace(123456)).to.eq('!!!!!6');
            expect(curriedReplace('123456')).to.eq('!!!!!6');
        });

        it('should replace numbers in float', function() {
            expect(curriedReplace(12.3456)).to.eq('!!!!!!6');
        });

        it('should replace numbers in string with custom dot', function() {
            expect(curriedReplace('12,3456')).to.eq('!!!!!!6');
        });

        it('should replace all symbols if number length < replace count', function() {
            expect(curriedReplace(123)).to.eq('!!!');
            expect(curriedReplace('1,23')).to.eq('!!!!');
            expect(curriedReplace(1)).to.eq('!');
            expect(curriedReplace('1')).to.eq('!');
        });
    });

    it('should fail', function() {
        var errorMessage = 'Value should be a number';

        expect(replace.bind(null, 'asd')).to.throw(errorMessage);
        expect(replace.bind(null, 'qwe.rty')).to.throw(errorMessage);
        expect(replace.bind(null, '123.456.789')).to.throw(errorMessage);
        expect(replace.bind(null, null)).to.throw(errorMessage);
        expect(replace.bind(null, undefined)).to.throw(errorMessage);
        expect(replace.bind(null, {})).to.throw(errorMessage);
        expect(replace.bind(null, {toString: function() {return '12.345'}})).to.throw(errorMessage);
        expect(replace.bind(null, Infinity)).to.throw(errorMessage);
        expect(replace.bind(null, -Infinity)).to.throw(errorMessage);
        expect(replace.bind(null, NaN)).to.throw(errorMessage);
    });
});
<|endoftext|>


<commit_before>/**
 * @flow
 */

import React from 'react';
import {
  AppState,
  Alert,
  Clipboard,
  Platform,
  RefreshControl,
  ScrollView,
  StatusBar,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';
import { createFocusAwareComponent, withNavigation } from '@expo/ex-navigation';
import { Constants } from 'expo';
import { connect } from 'react-redux';
import _ from 'lodash';

import addListenerWithNativeCallback from '../utils/addListenerWithNativeCallback';
import Alerts from '../constants/Alerts';
import Colors from '../constants/Colors';
import DevIndicator from '../components/DevIndicator';
import HistoryActions from '../redux/HistoryActions';
import OpenProjectByURLButton from '../components/OpenProjectByURLButton';
import NoProjectTools from '../components/NoProjectTools';
import NoProjectsOpen from '../components/NoProjectsOpen';
import ProjectTools from '../components/ProjectTools';
import SharedStyles from '../constants/SharedStyles';
import SmallProjectCard from '../components/SmallProjectCard';
import Store from '../redux/Store';
import Connectivity from '../api/Connectivity';
import getSnackId from '../utils/getSnackId';
import { isAuthenticated, authenticatedFetch } from '../api/helpers';

import extractReleaseChannel from '../utils/extractReleaseChannel';

const IS_RESTRICTED = Constants.isDevice && Platform.OS === 'ios';
const PROJECT_UPDATE_INTERVAL = 10000;

@createFocusAwareComponent
@withNavigation
@connect(data => ProjectsScreen.getDataProps(data))
export default class ProjectsScreen extends React.Component {
  props: {
    navigation: any,
    isFocused: boolean,
    dispatch: () => void,
    recentHistory: any,
    allHistory: any,
    navigator: any,
    isAuthenticated: boolean,
  };

  static route = {
    navigationBar: {
      title: 'Projects',
      ...Platform.select({
        ios: {
          renderRight: () => (Constants.isDevice ? null : <OpenProjectByURLButton />),
        },
      }),
    },
  };

  static getDataProps(data) {
    let { history } = data.history;

    return {
      recentHistory: history.take(10),
      allHistory: history,
      isAuthenticated: data.authTokens && data.authTokens.idToken,
    };
  }

  state = {
    projects: [],
    isNetworkAvailable: Connectivity.isAvailable(),
    isRefreshing: false,
  };

  componentDidMount() {
    AppState.addEventListener('change', this._maybeResumePollingFromAppState);
    Connectivity.addListener(this._updateConnectivity);
    this._startPollingForProjects();

    if (Platform.OS === 'android') {
      addListenerWithNativeCallback('ExponentKernel.showQRReader', async event => {
        this.props.navigation.showModal('qrCode');
        return { success: true };
      });
    }

    addListenerWithNativeCallback('ExponentKernel.addHistoryItem', async event => {
      let { manifestUrl, manifest, manifestString } = event;
      if (!manifest && manifestString) {
        manifest = JSON.parse(manifestString);
      }
      Store.dispatch(HistoryActions.addHistoryItem(manifestUrl, manifest));
    });
  }

  componentWillUnmount() {
    this._stopPollingForProjects();
    AppState.removeEventListener('change', this._maybeResumePollingFromAppState);
    Connectivity.removeListener(this._updateConnectivity);
  }

  render() {
    const { projects } = this.state;

    return (
      <View style={styles.container}>
        <ScrollView
          refreshControl={
            <RefreshControl
              refreshing={this.state.isRefreshing}
              onRefresh={this._handleRefreshAsync}
            />
          }
          key={
            /* note(brent): sticky headers break re-rendering scrollview */
            /* contents on sdk17, remove this in sdk18 */
            Platform.OS === 'ios' ? this.props.allHistory.count() : 'scroll-view'
          }
          stickyHeaderIndices={Platform.OS === 'ios' ? [0, 2, 4] : []}
          style={styles.container}
          contentContainerStyle={styles.contentContainer}>
          <View style={SharedStyles.sectionLabelContainer}>
            <Text style={SharedStyles.sectionLabelText}>
              {IS_RESTRICTED || Platform.OS === 'android' ? 'TOOLS' : 'CLIPBOARD'}
            </Text>
          </View>
          {this._renderProjectTools()}

          <View style={SharedStyles.sectionLabelContainer}>
            <View style={{ flexDirection: 'row', alignItems: 'center' }}>
              <DevIndicator
                style={{ marginRight: 7 }}
                isActive={projects && projects.length}
                isNetworkAvailable={this.state.isNetworkAvailable}
              />
              <Text style={SharedStyles.sectionLabelText}>RECENTLY IN DEVELOPMENT</Text>
            </View>
            <TouchableOpacity onPress={this._handlePressHelpProjects} style={styles.clearButton}>
              <Text style={styles.clearButtonText}>HELP</Text>
            </TouchableOpacity>
          </View>
          {this._renderProjects()}

          <View style={SharedStyles.sectionLabelContainer}>
            <Text style={SharedStyles.sectionLabelText}>RECENTLY OPENED</Text>
            <TouchableOpacity onPress={this._handlePressClearHistory} style={styles.clearButton}>
              <Text style={styles.clearButtonText}>CLEAR</Text>
            </TouchableOpacity>
          </View>

          {this._renderRecentHistory()}
          {this._renderConstants()}
        </ScrollView>

        <StatusBar barStyle="default" />
      </View>
    );
  }

  componentDidUpdate(prevProps) {
    if (!prevProps.isFocused && this.props.isFocused) {
      this._fetchProjectsAsync();
    }

    if (prevProps.isAuthenticated && !this.props.isAuthenticated) {
      // Remove all projects except Snack, because they are tied to device id
      this.setState(({ projects }) => ({
        projects: projects.filter(p => p.source === 'snack'),
      }));
    }
  }

  _updateConnectivity = (isAvailable: boolean): void => {
    if (isAvailable !== this.state.isNetworkAvailable) {
      this.setState({ isNetworkAvailable: isAvailable });
    }
  };

  _maybeResumePollingFromAppState = (nextAppState: string): void => {
    if (nextAppState === 'active' && !this._projectPolling) {
      this._startPollingForProjects();
    } else {
      this._stopPollingForProjects();
    }
  };

  _startPollingForProjects = async () => {
    this._handleRefreshAsync();
    this._projectPolling = setInterval(this._fetchProjectsAsync, PROJECT_UPDATE_INTERVAL);
  };

  _stopPollingForProjects = async () => {
    clearInterval(this._projectPolling);
    this._projectPolling = null;
  };

  _fetchProjectsAsync = async () => {
    let BASE_URL = __DEV__ ? 'https://staging.expo.io' : 'https://exp.host';
    let fetchStrategy = isAuthenticated() ? authenticatedFetch : fetch;
    let response = await fetchStrategy(
      `${BASE_URL}/--/api/v2/development-sessions?deviceId=${getSnackId()}`
    );
    let result = await response.json();
    let rawProjects = (result.data || []).reverse();
    let projects = _.uniqBy(rawProjects, p => p.url);
    this.setState({ projects });
  };

  _handleRefreshAsync = async () => {
    this.setState({ isRefreshing: true });

    try {
      await Promise.all([
        this._fetchProjectsAsync(),
        new Promise(resolve => setTimeout(resolve, 1000)),
      ]);
    } catch (e) {
      // not sure what to do here, maybe nothing?
    } finally {
      this.setState({ isRefreshing: false });
    }
  };

  _handlePressHelpProjects = () => {
    if (!this.state.isNetworkAvailable) {
      Alert.alert(
        'No network connection available',
        `You must be connected to the internet to view a list of your projects open in development.`
      );
    }

    let baseMessage = `Make sure you are signed in to the same Expo account on your computer and this app. Also verify that your computer is connected to the internet, and ideally to the same WiFi network as your mobile device. Lastly, ensure that you are using the latest version of exp or XDE. Pull to refresh to update.`;
    let message = Platform.select({
      ios: Constants.isDevice ? baseMessage : `${baseMessage} If this still doesn't work, press the + icon on the header to type the project URL manually.`,
      android: baseMessage,
    });
    Alert.alert('Troubleshooting', message);
  };

  _handlePressClearHistory = () => {
    this.props.dispatch(HistoryActions.clearHistory());
  };

  _renderProjectTools = () => {
    if (IS_RESTRICTED) {
      return (
        <View style={{ marginBottom: 10 }}>
          <NoProjectTools />
        </View>
      );
    } else {
      return <ProjectTools pollForUpdates={this.props.isFocused} />;
    }
  };

  _renderRecentHistory = () => {
    return this.props.allHistory.count() === 0
      ? this._renderEmptyRecentHistory()
      : this._renderRecentHistoryItems();
  };

  _renderEmptyRecentHistory = () => {
    return (
      <View style={SharedStyles.genericCardContainer} key="empty-history">
        <View style={SharedStyles.genericCardBody}>
          <Text style={[SharedStyles.faintText, { textAlign: 'center' }]}>
            You haven't opened any projects recently.
          </Text>
        </View>
      </View>
    );
  };

  _renderRecentHistoryItems = () => {
    const extractUsername = manifestUrl => {
      let username = manifestUrl.match(/@.*?\//)[0];
      if (!username) {
        return null;
      } else {
        return username.slice(0, username.length - 1);
      }
    };

    return this.props.recentHistory.map((project, i) => (
      <SmallProjectCard
        key={project.manifestUrl}
        iconUrl={project.manifest && project.manifest.iconUrl}
        releaseChannel={
          /* 28/11/17(brentvatne) - we can remove extractReleaseChannel in a couple of months
          when project history is unlikely to include any projects with release channels */
          (project.manifest && project.manifest.releaseChannel) ||
          extractReleaseChannel(project.manifestUrl)
        }
        projectName={project.manifest && project.manifest.name}
        username={
          project.manifestUrl.includes('exp://exp.host')
            ? extractUsername(project.manifestUrl)
            : null
        }
        projectUrl={project.manifestUrl}
        fullWidthBorder={i === this.props.recentHistory.count() - 1}
      />
    ));
  };

  _renderConstants = () => {
    return (
      <View style={styles.constantsContainer}>
        <Text style={styles.deviceIdText} onPress={this._copySnackIdToClipboard}>
          Device ID: {getSnackId()}
        </Text>
        <Text style={styles.expoVersionText} onPress={this._copyClientVersionToClipboard}>
          Client version: {Constants.expoVersion}
        </Text>
      </View>
    );
  };

  _copySnackIdToClipboard = () => {
    Clipboard.setString(getSnackId());
    this.props.navigator.showLocalAlert(
      'The device ID has been copied to your clipboard',
      Alerts.notice
    );
  };

  _copyClientVersionToClipboard = () => {
    Clipboard.setString(Constants.expoVersion);
    this.props.navigator.showLocalAlert(
      'The client version has been copied to your clipboard',
      Alerts.notice
    );
  };

  _renderProjects = () => {
    let { projects } = this.state;

    if (projects && projects.length) {
      return (
        <View style={styles.inDevelopmentContainer}>
          {projects.map((project, i) => (
            <SmallProjectCard
              icon={
                project.source === 'desktop'
                  ? require('../assets/cli.png')
                  : require('../assets/snack.png')
              }
              projectName={project.description}
              key={project.url}
              projectUrl={project.url}
              iconBorderStyle={{
                borderWidth: 1,
                borderColor: 'rgba(0, 0, 32, 0.1)',
              }}
              fullWidthBorder={i === projects.length - 1}
            />
          ))}
        </View>
      );
    } else {
      return <NoProjectsOpen isAuthenticated={this.props.isAuthenticated} />;
    }
  };
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: Colors.greyBackground,
  },
  inDevelopmentContainer: {
    marginBottom: 15,
  },
  infoContainer: {
    paddingTop: 13,
    flexDirection: 'column',
    alignSelf: 'stretch',
    paddingBottom: 10,
  },
  contentContainer: {
    paddingTop: 5,
  },
  clearButton: {
    position: 'absolute',
    right: Platform.OS === 'android' ? 15 : 0,
    top: Platform.OS === 'android' ? 12 : 0,
  },
  clearButtonText: {
    color: Colors.greyText,
    fontSize: 11,
    letterSpacing: 0.92,
    ...Platform.select({
      ios: {
        fontWeight: '500',
      },
    }),
  },
  constantsContainer: {
    paddingHorizontal: 20,
    paddingTop: 15,
    paddingBottom: 20,
    justifyContent: 'flex-end',
    alignItems: 'center',
    flex: 1,
  },
  deviceIdText: {
    color: 'rgba(0,0,0,0.3)',
    fontSize: 11,
    marginBottom: 5,
  },
  expoVersionText: {
    color: 'rgba(0,0,0,0.3)',
    fontSize: 11,
  },
});
<commit_msg>try/catch around fetch project<commit_after>/**
 * @flow
 */

import React from 'react';
import {
  AppState,
  Alert,
  Clipboard,
  Platform,
  RefreshControl,
  ScrollView,
  StatusBar,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';
import { createFocusAwareComponent, withNavigation } from '@expo/ex-navigation';
import { Constants } from 'expo';
import { connect } from 'react-redux';
import _ from 'lodash';

import addListenerWithNativeCallback from '../utils/addListenerWithNativeCallback';
import Alerts from '../constants/Alerts';
import Colors from '../constants/Colors';
import DevIndicator from '../components/DevIndicator';
import HistoryActions from '../redux/HistoryActions';
import OpenProjectByURLButton from '../components/OpenProjectByURLButton';
import NoProjectTools from '../components/NoProjectTools';
import NoProjectsOpen from '../components/NoProjectsOpen';
import ProjectTools from '../components/ProjectTools';
import SharedStyles from '../constants/SharedStyles';
import SmallProjectCard from '../components/SmallProjectCard';
import Store from '../redux/Store';
import Connectivity from '../api/Connectivity';
import getSnackId from '../utils/getSnackId';
import { isAuthenticated, authenticatedFetch } from '../api/helpers';

import extractReleaseChannel from '../utils/extractReleaseChannel';

const IS_RESTRICTED = Constants.isDevice && Platform.OS === 'ios';
const PROJECT_UPDATE_INTERVAL = 10000;

@createFocusAwareComponent
@withNavigation
@connect(data => ProjectsScreen.getDataProps(data))
export default class ProjectsScreen extends React.Component {
  props: {
    navigation: any,
    isFocused: boolean,
    dispatch: () => void,
    recentHistory: any,
    allHistory: any,
    navigator: any,
    isAuthenticated: boolean,
  };

  static route = {
    navigationBar: {
      title: 'Projects',
      ...Platform.select({
        ios: {
          renderRight: () => (Constants.isDevice ? null : <OpenProjectByURLButton />),
        },
      }),
    },
  };

  static getDataProps(data) {
    let { history } = data.history;

    return {
      recentHistory: history.take(10),
      allHistory: history,
      isAuthenticated: data.authTokens && data.authTokens.idToken,
    };
  }

  state = {
    projects: [],
    isNetworkAvailable: Connectivity.isAvailable(),
    isRefreshing: false,
  };

  componentDidMount() {
    AppState.addEventListener('change', this._maybeResumePollingFromAppState);
    Connectivity.addListener(this._updateConnectivity);
    this._startPollingForProjects();

    if (Platform.OS === 'android') {
      addListenerWithNativeCallback('ExponentKernel.showQRReader', async event => {
        this.props.navigation.showModal('qrCode');
        return { success: true };
      });
    }

    addListenerWithNativeCallback('ExponentKernel.addHistoryItem', async event => {
      let { manifestUrl, manifest, manifestString } = event;
      if (!manifest && manifestString) {
        manifest = JSON.parse(manifestString);
      }
      Store.dispatch(HistoryActions.addHistoryItem(manifestUrl, manifest));
    });
  }

  componentWillUnmount() {
    this._stopPollingForProjects();
    AppState.removeEventListener('change', this._maybeResumePollingFromAppState);
    Connectivity.removeListener(this._updateConnectivity);
  }

  render() {
    const { projects } = this.state;

    return (
      <View style={styles.container}>
        <ScrollView
          refreshControl={
            <RefreshControl
              refreshing={this.state.isRefreshing}
              onRefresh={this._handleRefreshAsync}
            />
          }
          key={
            /* note(brent): sticky headers break re-rendering scrollview */
            /* contents on sdk17, remove this in sdk18 */
            Platform.OS === 'ios' ? this.props.allHistory.count() : 'scroll-view'
          }
          stickyHeaderIndices={Platform.OS === 'ios' ? [0, 2, 4] : []}
          style={styles.container}
          contentContainerStyle={styles.contentContainer}>
          <View style={SharedStyles.sectionLabelContainer}>
            <Text style={SharedStyles.sectionLabelText}>
              {IS_RESTRICTED || Platform.OS === 'android' ? 'TOOLS' : 'CLIPBOARD'}
            </Text>
          </View>
          {this._renderProjectTools()}

          <View style={SharedStyles.sectionLabelContainer}>
            <View style={{ flexDirection: 'row', alignItems: 'center' }}>
              <DevIndicator
                style={{ marginRight: 7 }}
                isActive={projects && projects.length}
                isNetworkAvailable={this.state.isNetworkAvailable}
              />
              <Text style={SharedStyles.sectionLabelText}>RECENTLY IN DEVELOPMENT</Text>
            </View>
            <TouchableOpacity onPress={this._handlePressHelpProjects} style={styles.clearButton}>
              <Text style={styles.clearButtonText}>HELP</Text>
            </TouchableOpacity>
          </View>
          {this._renderProjects()}

          <View style={SharedStyles.sectionLabelContainer}>
            <Text style={SharedStyles.sectionLabelText}>RECENTLY OPENED</Text>
            <TouchableOpacity onPress={this._handlePressClearHistory} style={styles.clearButton}>
              <Text style={styles.clearButtonText}>CLEAR</Text>
            </TouchableOpacity>
          </View>

          {this._renderRecentHistory()}
          {this._renderConstants()}
        </ScrollView>

        <StatusBar barStyle="default" />
      </View>
    );
  }

  componentDidUpdate(prevProps) {
    if (!prevProps.isFocused && this.props.isFocused) {
      this._fetchProjectsAsync();
    }

    if (prevProps.isAuthenticated && !this.props.isAuthenticated) {
      // Remove all projects except Snack, because they are tied to device id
      this.setState(({ projects }) => ({
        projects: projects.filter(p => p.source === 'snack'),
      }));
    }
  }

  _updateConnectivity = (isAvailable: boolean): void => {
    if (isAvailable !== this.state.isNetworkAvailable) {
      this.setState({ isNetworkAvailable: isAvailable });
    }
  };

  _maybeResumePollingFromAppState = (nextAppState: string): void => {
    if (nextAppState === 'active' && !this._projectPolling) {
      this._startPollingForProjects();
    } else {
      this._stopPollingForProjects();
    }
  };

  _startPollingForProjects = async () => {
    this._handleRefreshAsync();
    this._projectPolling = setInterval(this._fetchProjectsAsync, PROJECT_UPDATE_INTERVAL);
  };

  _stopPollingForProjects = async () => {
    clearInterval(this._projectPolling);
    this._projectPolling = null;
  };

  _fetchProjectsAsync = async () => {
    try {
      let BASE_URL = __DEV__ ? 'https://staging.expo.io' : 'https://exp.host';
      let fetchStrategy = isAuthenticated() ? authenticatedFetch : fetch;
      let response = await fetchStrategy(
        `${BASE_URL}/--/api/v2/development-sessions?deviceId=${getSnackId()}`
      );
      let result = await response.json();
      let rawProjects = (result.data || []).reverse();
      let projects = _.uniqBy(rawProjects, p => p.url);
      this.setState({ projects });
    } catch (e) {
      // this doesn't really matter, we will try again later
      if (__DEV__) {
        console.log(e);
      }
    }
  };

  _handleRefreshAsync = async () => {
    this.setState({ isRefreshing: true });

    try {
      await Promise.all([
        this._fetchProjectsAsync(),
        new Promise(resolve => setTimeout(resolve, 1000)),
      ]);
    } catch (e) {
      // not sure what to do here, maybe nothing?
    } finally {
      this.setState({ isRefreshing: false });
    }
  };

  _handlePressHelpProjects = () => {
    if (!this.state.isNetworkAvailable) {
      Alert.alert(
        'No network connection available',
        `You must be connected to the internet to view a list of your projects open in development.`
      );
    }

    let baseMessage = `Make sure you are signed in to the same Expo account on your computer and this app. Also verify that your computer is connected to the internet, and ideally to the same WiFi network as your mobile device. Lastly, ensure that you are using the latest version of exp or XDE. Pull to refresh to update.`;
    let message = Platform.select({
      ios: Constants.isDevice ? baseMessage : `${baseMessage} If this still doesn't work, press the + icon on the header to type the project URL manually.`,
      android: baseMessage,
    });
    Alert.alert('Troubleshooting', message);
  };

  _handlePressClearHistory = () => {
    this.props.dispatch(HistoryActions.clearHistory());
  };

  _renderProjectTools = () => {
    if (IS_RESTRICTED) {
      return (
        <View style={{ marginBottom: 10 }}>
          <NoProjectTools />
        </View>
      );
    } else {
      return <ProjectTools pollForUpdates={this.props.isFocused} />;
    }
  };

  _renderRecentHistory = () => {
    return this.props.allHistory.count() === 0
      ? this._renderEmptyRecentHistory()
      : this._renderRecentHistoryItems();
  };

  _renderEmptyRecentHistory = () => {
    return (
      <View style={SharedStyles.genericCardContainer} key="empty-history">
        <View style={SharedStyles.genericCardBody}>
          <Text style={[SharedStyles.faintText, { textAlign: 'center' }]}>
            You haven't opened any projects recently.
          </Text>
        </View>
      </View>
    );
  };

  _renderRecentHistoryItems = () => {
    const extractUsername = manifestUrl => {
      let username = manifestUrl.match(/@.*?\//)[0];
      if (!username) {
        return null;
      } else {
        return username.slice(0, username.length - 1);
      }
    };

    return this.props.recentHistory.map((project, i) => (
      <SmallProjectCard
        key={project.manifestUrl}
        iconUrl={project.manifest && project.manifest.iconUrl}
        releaseChannel={
          /* 28/11/17(brentvatne) - we can remove extractReleaseChannel in a couple of months
          when project history is unlikely to include any projects with release channels */
          (project.manifest && project.manifest.releaseChannel) ||
          extractReleaseChannel(project.manifestUrl)
        }
        projectName={project.manifest && project.manifest.name}
        username={
          project.manifestUrl.includes('exp://exp.host')
            ? extractUsername(project.manifestUrl)
            : null
        }
        projectUrl={project.manifestUrl}
        fullWidthBorder={i === this.props.recentHistory.count() - 1}
      />
    ));
  };

  _renderConstants = () => {
    return (
      <View style={styles.constantsContainer}>
        <Text style={styles.deviceIdText} onPress={this._copySnackIdToClipboard}>
          Device ID: {getSnackId()}
        </Text>
        <Text style={styles.expoVersionText} onPress={this._copyClientVersionToClipboard}>
          Client version: {Constants.expoVersion}
        </Text>
      </View>
    );
  };

  _copySnackIdToClipboard = () => {
    Clipboard.setString(getSnackId());
    this.props.navigator.showLocalAlert(
      'The device ID has been copied to your clipboard',
      Alerts.notice
    );
  };

  _copyClientVersionToClipboard = () => {
    Clipboard.setString(Constants.expoVersion);
    this.props.navigator.showLocalAlert(
      'The client version has been copied to your clipboard',
      Alerts.notice
    );
  };

  _renderProjects = () => {
    let { projects } = this.state;

    if (projects && projects.length) {
      return (
        <View style={styles.inDevelopmentContainer}>
          {projects.map((project, i) => (
            <SmallProjectCard
              icon={
                project.source === 'desktop'
                  ? require('../assets/cli.png')
                  : require('../assets/snack.png')
              }
              projectName={project.description}
              key={project.url}
              projectUrl={project.url}
              iconBorderStyle={{
                borderWidth: 1,
                borderColor: 'rgba(0, 0, 32, 0.1)',
              }}
              fullWidthBorder={i === projects.length - 1}
            />
          ))}
        </View>
      );
    } else {
      return <NoProjectsOpen isAuthenticated={this.props.isAuthenticated} />;
    }
  };
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: Colors.greyBackground,
  },
  inDevelopmentContainer: {
    marginBottom: 15,
  },
  infoContainer: {
    paddingTop: 13,
    flexDirection: 'column',
    alignSelf: 'stretch',
    paddingBottom: 10,
  },
  contentContainer: {
    paddingTop: 5,
  },
  clearButton: {
    position: 'absolute',
    right: Platform.OS === 'android' ? 15 : 0,
    top: Platform.OS === 'android' ? 12 : 0,
  },
  clearButtonText: {
    color: Colors.greyText,
    fontSize: 11,
    letterSpacing: 0.92,
    ...Platform.select({
      ios: {
        fontWeight: '500',
      },
    }),
  },
  constantsContainer: {
    paddingHorizontal: 20,
    paddingTop: 15,
    paddingBottom: 20,
    justifyContent: 'flex-end',
    alignItems: 'center',
    flex: 1,
  },
  deviceIdText: {
    color: 'rgba(0,0,0,0.3)',
    fontSize: 11,
    marginBottom: 5,
  },
  expoVersionText: {
    color: 'rgba(0,0,0,0.3)',
    fontSize: 11,
  },
});
<|endoftext|>


<commit_before>'use strict'

const Compiler = render.Compiler = require('./compiler')
const lazy = require('lazyrequire')(require)
const Parser = require('jade').Parser
const rethrow = require('jade').runtime.rethrow
const beautify = lazy('js-beautify')
const addWith = require('with')

module.exports = render

/**
 * Runtime included when you create a template.
 * You are expected to install these yourself.
 */

render.runtime = `
var h = require('virtual-dom/h');
`

/**
 * Variables that you don't want to bother `with`ing.
 */

render.globals = [
  'require',
]

/**
 * Compile the jade source into a function string with the signature:
 *
 * (locals) => VNode
 *
 * You are expected to `eval()` the result if you want an actual function.
 */

const DEFAULT_OPTIONS = {
  name: 'render',
  vdom: 'virtual-dom',
}

function render(str, options) {
  options = Object.assign({}, options, DEFAULT_OPTIONS)

  const name = options.name

  // parse
  const ParserCls = options.parser || Parser
  const parser = new ParserCls(str, options.filename)
  let tokens
  try {
    tokens = parser.parse()
  } catch (err) {
    const context = parser.context()
    rethrow(err, context.filename, context.lexer.lineno, context.input)
  }

  // compile
  const compiler = new Compiler(tokens, options)

  // jade -> code
  let out = render.runtime
  try {
    out += compiler.compile()
  } catch (err) {
    rethrow(err, err.filename, err.line, parser.input)
  }

  // add locals
  out = addWith('locals', out, render.globals)

  // wrap in a function
  out = `
    function ${name}(locals) {
      locals = locals || {};
      ${out}
    }
  `

  // beautify
  if (options.pretty) {
    out = beautify().js_beautify(out, { indent_size: 2 })
  }

  return out
}
<commit_msg>start vdom config<commit_after>'use strict'

const Compiler = render.Compiler = require('./compiler')
const lazy = require('lazyrequire')(require)
const Parser = require('jade').Parser
const rethrow = require('jade').runtime.rethrow
const beautify = lazy('js-beautify')
const addWith = require('with')

module.exports = render

/**
 * Configurations for Virtual DOM libraries.
 * You are expected to install runtimes yourself.
 */
const VDOM_CONFIG = {
  'snabbdom': {
    runtime: `var h = require('snabbdom/h');`,
  },
  'virtual-dom': {
    runtime: `var h = require('virtual-dom/h');`,
  },
}

/**
 * Variables that you don't want to bother `with`ing.
 */
render.globals = [
  'require',
]

/**
 * Default renderer options.
 */
const DEFAULT_OPTIONS = {
  name: 'render',
  vdom: 'virtual-dom',
}

/**
 * Compile the jade source into a function string with the signature:
 *
 * (locals) => VNode
 *
 * You are expected to `eval()` the result if you want an actual function.
 */
function render(str, options) {
  options = Object.assign({}, options, DEFAULT_OPTIONS)

  const name = options.name
  const vdom = VDOM_CONFIG[options.vdom]
  const runtime = options.runtime || vdom.runtime

  // parse
  const ParserCls = options.parser || Parser
  const parser = new ParserCls(str, options.filename)
  let tokens
  try {
    tokens = parser.parse()
  } catch (err) {
    const context = parser.context()
    rethrow(err, context.filename, context.lexer.lineno, context.input)
  }

  // compile
  const compiler = new Compiler(tokens, options)

  // jade -> code
  let out = runtime
  try {
    out += compiler.compile()
  } catch (err) {
    rethrow(err, err.filename, err.line, parser.input)
  }

  // add locals
  out = addWith('locals', out, render.globals)

  // wrap in a function
  out = `
    function ${name}(locals) {
      locals = locals || {};
      ${out}
    }
  `

  // beautify
  if (options.pretty) {
    out = beautify().js_beautify(out, { indent_size: 2 })
  }

  return out
}
<|endoftext|>


<commit_before>// Copyright 2002-2015, University of Colorado Boulder

/**
 * Grunt configuration file for PhET projects.
 * Requires a package.json file containing project settings.
 * Tasks share information via global.phet, see individual tasks for details.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 * @author Jon Olson
 * @author Sam Reid
 * @author John Blanco
 */

// built-in node APIs
var assert = require( 'assert' );
var fs = require( 'fs' );

// 3rd-party packages
/* jshint -W079 */
var _ = require( '../../../sherpa/lodash-2.4.1.min' ); // allow _ to be redefined, contrary to jshintOptions.js
/* jshint +W079 */

// PhET custom grunt tasks
var afterRequirejsBuild = require( '../../../chipper/js/grunt/afterRequirejsBuild' );
var beforeRequirejsBuild = require( '../../../chipper/js/grunt/beforeRequirejsBuild' );
var bumpVersion = require( '../../../chipper/js/grunt/bumpVersion' );
var checkoutShas = require( '../../../chipper/js/grunt/checkoutShas' );
var cloneDependencies = require( '../../../chipper/js/grunt/cloneDependencies' );
var createSim = require( '../../../chipper/js/grunt/createSim' );
var generateLicenseText = require( '../../../chipper/js/grunt/generateLicenseText' );
var generateREADME = require( '../../../chipper/js/grunt/generateREADME' );
var generateThumbnails = require( '../../../chipper/js/grunt/generateThumbnails' );
var pullAll = require( '../../../chipper/js/grunt/pullAll' );
var stringReport = require( '../../../chipper/js/grunt/stringReport' );

//TODO look at why this is necessary
/*
 * In Node, global is the global namespace object.
 * Register fs as a global so it can be accessed through the requirejs build system. Text.js plugin
 * may have a superior way to handle this but I (SR) couldn't get it working after a small amount of effort.
 */
global.fs = fs;

module.exports = function( grunt ) {
  'use strict';

  var FALLBACK_LOCAL = 'en';

  // Read package.json, verify that it contains properties required by all PhET repositories
  assert( fs.existsSync( 'package.json' ), 'repository must have a package.json' );
  var pkg = grunt.file.readJSON( 'package.json' );
  assert( pkg.preload, 'preload missing from package.json' );
  if ( grunt.option( 'together' ) ) {
    console.log( 'Adding additional preload files for together' );
    // add the additional preload files needed to support data exchange using together.js
    pkg.preload.push( '../together/js/together.js' );
    pkg.preload.push( '../together/js/SimIFrameAPI.js' );
    pkg.preload.push( '../together/js/arch.js' );
    pkg.preload.push( '../together/js/datamite.js' );
    pkg.preload.push( '../together/js/api/Common.js' );
    pkg.preload.push( '../together/js/api/' + pkg.name + '-api.js' );
  }
  assert( pkg.name, 'name missing from package.json' );
  assert( pkg.version, 'version missing from package.json' );
  assert( pkg.license, 'license missing from package.json' );

  var globalDefs = {
    // global assertions
    assert: false,
    assertSlow: false,
    // scenery logging
    sceneryLog: false,
    sceneryLayerLog: false,
    sceneryEventLog: false,
    sceneryAccessibilityLog: false,
    phetAllocation: false
  };

  // Project configuration.
  grunt.initConfig( {
    /*
     * Read in the project settings from the package.json file into the pkg property.
     * This allows us to refer to project settings from within this config file.
     */
    pkg: pkg,

    // configure the RequireJS plugin
    requirejs: {

      // builds the minified script
      build: {
        options: {
          almond: true,
          mainConfigFile: 'js/<%= pkg.name %>-config.js',
          out: 'build/<%= pkg.name %>.min.js',
          name: '<%= pkg.name %>-config',

          // Minification strategy.  Put this to none if you want to debug a non-minified but compiled version
          optimize: 'uglify2',
          wrap: true,
//            generateSourceMaps: true, //#42 commented out this line until source maps are fixed
          preserveLicenseComments: false,
          uglify2: {
            output: {
              inline_script: true // escape </script
            },
            compress: {
              global_defs: globalDefs,
              dead_code: true
            }
          },

          //stub out the plugins so their source code won't be included in the minified file
          stubModules: [ 'string', 'audio', 'image' ]
        }
      }
    },

    // configure the JSHint plugin
    jshint: {

      // source files that are specific to this repository
      repoFiles: [ 'js/**/*.js' ],

      // All source files for this repository (repository-specific and dependencies).
      // Excludes kite/js/parser/svgPath.js, which is auto-generated.
      allFiles: [ _.map( pkg.phetLibs, function( repo ) { return '../' + repo + '/js/**/*.js'; } ), '!../kite/js/parser/svgPath.js' ],

      // reference external JSHint options in jshintOptions.js
      options: require( './jshintOptions' )
    }
  } );

  //---------------------------------------------------------------------------------------------------------------
  // Primary tasks
  //---------------------------------------------------------------------------------------------------------------

  // Default task ('grunt')
  grunt.registerTask( 'default', 'Builds the English HTML', [ 'build' ] );

  grunt.registerTask( 'build',
    'Builds the simulation:\n' +
    'with no options, builds HTML for English only\n' +
    '--locales=* : all locales in strings/ directory\n' +
    '--locales=fr : French\n' +
    '--locales=ar,fr,es : Arabic, French and Spanish (comma separated locales)\n' +
                                                                                '--localesRepo=$repo : all locales in another repository\'s strings/ directory, ignored if --locales is present\n',
    [ 'lint-all', 'build-no-lint' ] );

  grunt.registerTask( 'build-no-lint',
    'identical to "build", but does not run "lint-all"',
    [ 'clean', 'generate-license-text', 'before-requirejs-build', 'requirejs:build', 'after-requirejs-build' ] );

  grunt.registerTask( 'lint', 'lint js files that are specific to this repository', [ 'jshint:repoFiles' ] );

  grunt.registerTask( 'lint-all', 'lint all js files that are required to build this repository', [ 'jshint:allFiles' ] );

  grunt.registerTask( 'clean',
    'Erases the build/ directory and all its contents, and recreates the build/ directory',
    function() {
      if ( fs.existsSync( 'build' ) ) {
        grunt.log.writeln( 'Cleaning build directory' );
        grunt.file.delete( 'build' );
      }
      grunt.file.mkdir( 'build' );
    } );

  grunt.registerTask( 'generate-license-text',
    'Generates the license text that will be written to the HTML file',
    function() {
      assert( pkg.preload, 'preload required in package.json' );
      generateLicenseText( grunt, pkg.preload );
    } );

  grunt.registerTask( 'before-requirejs-build',
    '(internal use only) Do things before the requirejs:build task',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.preload, 'preload missing from package.json' );
      beforeRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  grunt.registerTask( 'after-requirejs-build',
    '(internal use only) Do things after the requirejs:build task',
    function() {
      afterRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  //---------------------------------------------------------------------------------------------------------------
  // Utility tasks
  //---------------------------------------------------------------------------------------------------------------

  grunt.registerTask( 'checkout-shas',
    'Check out shas for a project, as specified in dependencies.json',
    function() {
      checkoutShas( grunt, pkg.name, false );
    } );

  grunt.registerTask( 'checkout-master',
    'Check out master branch for all dependencies, as specified in dependencies.json',
    function() {
      checkoutShas( grunt, pkg.name, true );
    } );

  grunt.registerTask( 'create-sim',
    'Creates a sim based on the simula-rasa template.\n' +
    '--name="string" : the repository name\n' +
    '--author="string" : the author name\n' +
    '--title="string" : (optional) the simulation title\n' +
    '--clean=true : (optional) deletes the repository directory if it exists',
    function() {
      createSim( grunt, grunt.option( 'name' ), grunt.option( 'author' ), grunt.option( 'title' ), grunt.option( 'clean' ) );
    } );

  grunt.registerTask( 'generate-published-README',
    'Generates README.md file for a published simulation.',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.simTitleStringKey, 'simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phetLibs, pkg.simTitleStringKey, true /* published */ );
    } );

  grunt.registerTask( 'generate-unpublished-README',
    'Generates README.md file for an unpublished simulation.',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.simTitleStringKey, 'simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phetLibs, pkg.simTitleStringKey, false /* published */ );
    } );

  grunt.registerTask( 'bump-version',
    'This task updates the last value in the version by one. For example from 0.0.0-dev.12 to 0.0.0-dev.13.' +
    'This updates the package.json and js/version.js files, and commits + pushes to git.' +
    'BEWARE: Do not run this task unless your git is clean, otherwise it will commit other work on your repo as well.',
    function() {
      bumpVersion( grunt, pkg.version );
    } );

  grunt.registerTask( 'pull-all',
    'Pull all sibling repositories',
    function() {
      pullAll( grunt );
    } );

  grunt.registerTask( 'string-report',
    'After doing a build, reports on which strings are missing for each locale that was built.',
    function() {
      stringReport( grunt, pkg.name, FALLBACK_LOCAL );
    } );

  //TODO is this vestigial?
  grunt.registerTask( 'clone-dependencies',
    'Clones all dependencies of a project, as listed in package.json phetLibs entry',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      cloneDependencies( grunt, pkg.name, pkg.phetLibs );
    } );

  grunt.registerTask( 'generate-128-thumbnail', 'Generate 128x84 thumbnail', function() {
    generateThumbnails( grunt, pkg.name, 128, 84 );
  } );

  grunt.registerTask( 'generate-600-thumbnail', 'Generate 600x394 thumbnail', function() {
    generateThumbnails( grunt, pkg.name, 600, 394 );
  } );

  grunt.registerTask( 'generate-thumbnails', 'Generate thumbnails', [ 'generate-128-thumbnail', 'generate-600-thumbnail' ] );

  /*
   * Load tasks from grunt plugins that have been installed locally using npm.
   * Put these in package.json and run 'npm install' before running grunt.
   */
  grunt.loadNpmTasks( 'grunt-requirejs' );
  grunt.loadNpmTasks( 'grunt-contrib-jshint' );
};
<commit_msg>fixed an issue with the way the --together option was listed in the "build" task, see #130<commit_after>// Copyright 2002-2015, University of Colorado Boulder

/**
 * Grunt configuration file for PhET projects.
 * Requires a package.json file containing project settings.
 * Tasks share information via global.phet, see individual tasks for details.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 * @author Jon Olson
 * @author Sam Reid
 * @author John Blanco
 */

// built-in node APIs
var assert = require( 'assert' );
var fs = require( 'fs' );

// 3rd-party packages
/* jshint -W079 */
var _ = require( '../../../sherpa/lodash-2.4.1.min' ); // allow _ to be redefined, contrary to jshintOptions.js
/* jshint +W079 */

// PhET custom grunt tasks
var afterRequirejsBuild = require( '../../../chipper/js/grunt/afterRequirejsBuild' );
var beforeRequirejsBuild = require( '../../../chipper/js/grunt/beforeRequirejsBuild' );
var bumpVersion = require( '../../../chipper/js/grunt/bumpVersion' );
var checkoutShas = require( '../../../chipper/js/grunt/checkoutShas' );
var cloneDependencies = require( '../../../chipper/js/grunt/cloneDependencies' );
var createSim = require( '../../../chipper/js/grunt/createSim' );
var generateLicenseText = require( '../../../chipper/js/grunt/generateLicenseText' );
var generateREADME = require( '../../../chipper/js/grunt/generateREADME' );
var generateThumbnails = require( '../../../chipper/js/grunt/generateThumbnails' );
var pullAll = require( '../../../chipper/js/grunt/pullAll' );
var stringReport = require( '../../../chipper/js/grunt/stringReport' );

//TODO look at why this is necessary
/*
 * In Node, global is the global namespace object.
 * Register fs as a global so it can be accessed through the requirejs build system. Text.js plugin
 * may have a superior way to handle this but I (SR) couldn't get it working after a small amount of effort.
 */
global.fs = fs;

module.exports = function( grunt ) {
  'use strict';

  var FALLBACK_LOCAL = 'en';

  // Read package.json, verify that it contains properties required by all PhET repositories
  assert( fs.existsSync( 'package.json' ), 'repository must have a package.json' );
  var pkg = grunt.file.readJSON( 'package.json' );
  assert( pkg.preload, 'preload missing from package.json' );
  if ( grunt.option( 'together' ) ) {
    console.log( 'Adding additional preload files for together' );
    // add the additional preload files needed to support data exchange using together.js
    pkg.preload.push( '../together/js/together.js' );
    pkg.preload.push( '../together/js/SimIFrameAPI.js' );
    pkg.preload.push( '../together/js/arch.js' );
    pkg.preload.push( '../together/js/datamite.js' );
    pkg.preload.push( '../together/js/api/Common.js' );
    pkg.preload.push( '../together/js/api/' + pkg.name + '-api.js' );
  }
  assert( pkg.name, 'name missing from package.json' );
  assert( pkg.version, 'version missing from package.json' );
  assert( pkg.license, 'license missing from package.json' );

  var globalDefs = {
    // global assertions
    assert: false,
    assertSlow: false,
    // scenery logging
    sceneryLog: false,
    sceneryLayerLog: false,
    sceneryEventLog: false,
    sceneryAccessibilityLog: false,
    phetAllocation: false
  };

  // Project configuration.
  grunt.initConfig( {
    /*
     * Read in the project settings from the package.json file into the pkg property.
     * This allows us to refer to project settings from within this config file.
     */
    pkg: pkg,

    // configure the RequireJS plugin
    requirejs: {

      // builds the minified script
      build: {
        options: {
          almond: true,
          mainConfigFile: 'js/<%= pkg.name %>-config.js',
          out: 'build/<%= pkg.name %>.min.js',
          name: '<%= pkg.name %>-config',

          // Minification strategy.  Put this to none if you want to debug a non-minified but compiled version
          optimize: 'uglify2',
          wrap: true,
//            generateSourceMaps: true, //#42 commented out this line until source maps are fixed
          preserveLicenseComments: false,
          uglify2: {
            output: {
              inline_script: true // escape </script
            },
            compress: {
              global_defs: globalDefs,
              dead_code: true
            }
          },

          //stub out the plugins so their source code won't be included in the minified file
          stubModules: [ 'string', 'audio', 'image' ]
        }
      }
    },

    // configure the JSHint plugin
    jshint: {

      // source files that are specific to this repository
      repoFiles: [ 'js/**/*.js' ],

      // All source files for this repository (repository-specific and dependencies).
      // Excludes kite/js/parser/svgPath.js, which is auto-generated.
      allFiles: [ _.map( pkg.phetLibs, function( repo ) { return '../' + repo + '/js/**/*.js'; } ), '!../kite/js/parser/svgPath.js' ],

      // reference external JSHint options in jshintOptions.js
      options: require( './jshintOptions' )
    }
  } );

  //---------------------------------------------------------------------------------------------------------------
  // Primary tasks
  //---------------------------------------------------------------------------------------------------------------

  // Default task ('grunt')
  grunt.registerTask( 'default', 'Builds the English HTML', [ 'build' ] );

  grunt.registerTask( 'build',
    'Builds the simulation:\n' +
    'with no options, builds HTML for English only\n' +
    '--locales=* : all locales in strings/ directory\n' +
    '--locales=fr : French\n' +
    '--locales=ar,fr,es : Arabic, French and Spanish (comma separated locales)\n' +
                                                                                '--localesRepo=$repo : all locales in another repository\'s strings/ directory, ignored if --locales is present\n' +
                                                                                '--together : adds additional preload files needed to support together.js',
    [ 'lint-all', 'build-no-lint' ] );

  grunt.registerTask( 'build-no-lint',
    'identical to "build", but does not run "lint-all"',
    [ 'clean', 'generate-license-text', 'before-requirejs-build', 'requirejs:build', 'after-requirejs-build' ] );

  grunt.registerTask( 'lint', 'lint js files that are specific to this repository', [ 'jshint:repoFiles' ] );

  grunt.registerTask( 'lint-all', 'lint all js files that are required to build this repository', [ 'jshint:allFiles' ] );

  grunt.registerTask( 'clean',
    'Erases the build/ directory and all its contents, and recreates the build/ directory',
    function() {
      if ( fs.existsSync( 'build' ) ) {
        grunt.log.writeln( 'Cleaning build directory' );
        grunt.file.delete( 'build' );
      }
      grunt.file.mkdir( 'build' );
    } );

  grunt.registerTask( 'generate-license-text',
    'Generates the license text that will be written to the HTML file',
    function() {
      assert( pkg.preload, 'preload required in package.json' );
      generateLicenseText( grunt, pkg.preload );
    } );

  grunt.registerTask( 'before-requirejs-build',
    '(internal use only) Do things before the requirejs:build task',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.preload, 'preload missing from package.json' );
      beforeRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  grunt.registerTask( 'after-requirejs-build',
    '(internal use only) Do things after the requirejs:build task',
    function() {
      afterRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  //---------------------------------------------------------------------------------------------------------------
  // Utility tasks
  //---------------------------------------------------------------------------------------------------------------

  grunt.registerTask( 'checkout-shas',
    'Check out shas for a project, as specified in dependencies.json',
    function() {
      checkoutShas( grunt, pkg.name, false );
    } );

  grunt.registerTask( 'checkout-master',
    'Check out master branch for all dependencies, as specified in dependencies.json',
    function() {
      checkoutShas( grunt, pkg.name, true );
    } );

  grunt.registerTask( 'create-sim',
    'Creates a sim based on the simula-rasa template.\n' +
    '--name="string" : the repository name\n' +
    '--author="string" : the author name\n' +
    '--title="string" : (optional) the simulation title\n' +
    '--clean=true : (optional) deletes the repository directory if it exists',
    function() {
      createSim( grunt, grunt.option( 'name' ), grunt.option( 'author' ), grunt.option( 'title' ), grunt.option( 'clean' ) );
    } );

  grunt.registerTask( 'generate-published-README',
    'Generates README.md file for a published simulation.',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.simTitleStringKey, 'simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phetLibs, pkg.simTitleStringKey, true /* published */ );
    } );

  grunt.registerTask( 'generate-unpublished-README',
    'Generates README.md file for an unpublished simulation.',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      assert( pkg.simTitleStringKey, 'simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phetLibs, pkg.simTitleStringKey, false /* published */ );
    } );

  grunt.registerTask( 'bump-version',
    'This task updates the last value in the version by one. For example from 0.0.0-dev.12 to 0.0.0-dev.13.' +
    'This updates the package.json and js/version.js files, and commits + pushes to git.' +
    'BEWARE: Do not run this task unless your git is clean, otherwise it will commit other work on your repo as well.',
    function() {
      bumpVersion( grunt, pkg.version );
    } );

  grunt.registerTask( 'pull-all',
    'Pull all sibling repositories',
    function() {
      pullAll( grunt );
    } );

  grunt.registerTask( 'string-report',
    'After doing a build, reports on which strings are missing for each locale that was built.',
    function() {
      stringReport( grunt, pkg.name, FALLBACK_LOCAL );
    } );

  //TODO is this vestigial?
  grunt.registerTask( 'clone-dependencies',
    'Clones all dependencies of a project, as listed in package.json phetLibs entry',
    function() {
      assert( pkg.phetLibs, 'phetLibs missing from package.json' );
      cloneDependencies( grunt, pkg.name, pkg.phetLibs );
    } );

  grunt.registerTask( 'generate-128-thumbnail', 'Generate 128x84 thumbnail', function() {
    generateThumbnails( grunt, pkg.name, 128, 84 );
  } );

  grunt.registerTask( 'generate-600-thumbnail', 'Generate 600x394 thumbnail', function() {
    generateThumbnails( grunt, pkg.name, 600, 394 );
  } );

  grunt.registerTask( 'generate-thumbnails', 'Generate thumbnails', [ 'generate-128-thumbnail', 'generate-600-thumbnail' ] );

  /*
   * Load tasks from grunt plugins that have been installed locally using npm.
   * Put these in package.json and run 'npm install' before running grunt.
   */
  grunt.loadNpmTasks( 'grunt-requirejs' );
  grunt.loadNpmTasks( 'grunt-contrib-jshint' );
};
<|endoftext|>