const fs = require('fs') const gulp = require('gulp') const watch = require('gulp-watch') const source = require('vinyl-source-stream') const buffer = require('vinyl-buffer') const log = require('fancy-log') const { assign } = require('lodash') const watchify = require('watchify') const browserify = require('browserify') const envify = require('envify/custom') const sourcemaps = require('gulp-sourcemaps') const sesify = require('sesify') const terser = require('gulp-terser-js') const pify = require('pify') const endOfStream = pify(require('end-of-stream')) const { makeStringTransform } = require('browserify-transform-tools') const conf = require('rc')('metamask', { INFURA_PROJECT_ID: process.env.INFURA_PROJECT_ID, SEGMENT_HOST: process.env.SEGMENT_HOST, SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY, SEGMENT_LEGACY_WRITE_KEY: process.env.SEGMENT_LEGACY_WRITE_KEY, }) const packageJSON = require('../../package.json') const { createTask, composeParallel, composeSeries, runInChildProcess, } = require('./task') module.exports = createScriptTasks const dependencies = Object.keys( (packageJSON && packageJSON.dependencies) || {}, ) const materialUIDependencies = ['@material-ui/core'] const reactDepenendencies = dependencies.filter((dep) => dep.match(/react/u)) const d3Dependencies = ['c3', 'd3'] const externalDependenciesMap = { background: ['3box'], ui: [...materialUIDependencies, ...reactDepenendencies, ...d3Dependencies], } function createScriptTasks({ browserPlatforms, livereload }) { // internal tasks const core = { // dev tasks (live reload) dev: createTasksForBuildJsExtension({ taskPrefix: 'scripts:core:dev', devMode: true, }), testDev: createTasksForBuildJsExtension({ taskPrefix: 'scripts:core:test-live', devMode: true, testing: true, }), // built for CI tests test: createTasksForBuildJsExtension({ taskPrefix: 'scripts:core:test', testing: true, }), // production prod: createTasksForBuildJsExtension({ taskPrefix: 'scripts:core:prod' }), } const deps = { background: createTasksForBuildJsDeps({ filename: 'bg-libs', key: 'background', }), ui: createTasksForBuildJsDeps({ filename: 'ui-libs', key: 'ui' }), } // high level tasks const prod = composeParallel(deps.background, deps.ui, core.prod) const { dev, testDev } = core const test = composeParallel(deps.background, deps.ui, core.test) return { prod, dev, testDev, test } function createTasksForBuildJsDeps({ key, filename }) { return createTask( `scripts:deps:${key}`, bundleTask({ label: filename, filename: `${filename}.js`, buildLib: true, dependenciesToBundle: externalDependenciesMap[key], devMode: false, }), ) } function createTasksForBuildJsExtension({ taskPrefix, devMode, testing }) { const standardBundles = ['background', 'ui', 'phishing-detect'] const standardSubtasks = standardBundles.map((filename) => { return createTask( `${taskPrefix}:${filename}`, createBundleTaskForBuildJsExtensionNormal({ filename, devMode, testing, }), ) }) // inpage must be built before contentscript // because inpage bundle result is included inside contentscript const contentscriptSubtask = createTask( `${taskPrefix}:contentscript`, createTaskForBuildJsExtensionContentscript({ devMode, testing }), ) // task for initiating livereload const initiateLiveReload = async () => { if (devMode) { // trigger live reload when the bundles are updated // this is not ideal, but overcomes the limitations: // - run from the main process (not child process tasks) // - after the first build has completed (thus the timeout) // - build tasks never "complete" when run with livereload + child process setTimeout(() => { watch('./dist/*/*.js', (event) => { livereload.changed(event.path) }) }, 75e3) } } // make each bundle run in a separate process const allSubtasks = [ ...standardSubtasks, contentscriptSubtask, ].map((subtask) => runInChildProcess(subtask)) // const allSubtasks = [...standardSubtasks, contentscriptSubtask].map(subtask => (subtask)) // make a parent task that runs each task in a child thread return composeParallel(initiateLiveReload, ...allSubtasks) } function createBundleTaskForBuildJsExtensionNormal({ filename, devMode, testing, }) { return bundleTask({ label: filename, filename: `${filename}.js`, filepath: `./app/scripts/${filename}.js`, externalDependencies: devMode ? undefined : externalDependenciesMap[filename], devMode, testing, }) } function createTaskForBuildJsExtensionContentscript({ devMode, testing }) { const inpage = 'inpage' const contentscript = 'contentscript' return composeSeries( bundleTask({ label: inpage, filename: `${inpage}.js`, filepath: `./app/scripts/${inpage}.js`, externalDependencies: devMode ? undefined : externalDependenciesMap[inpage], devMode, testing, }), bundleTask({ label: contentscript, filename: `${contentscript}.js`, filepath: `./app/scripts/${contentscript}.js`, externalDependencies: devMode ? undefined : externalDependenciesMap[contentscript], devMode, testing, }), ) } function bundleTask(opts) { let bundler return performBundle async function performBundle() { // initialize bundler if not available yet // dont create bundler until task is actually run if (!bundler) { bundler = generateBundler(opts, performBundle) // output build logs to terminal bundler.on('log', log) } let buildStream = bundler.bundle() // handle errors buildStream.on('error', (err) => { beep() if (opts.devMode) { console.warn(err.stack) } else { throw err } }) // process bundles buildStream = buildStream // convert bundle stream to gulp vinyl stream .pipe(source(opts.filename)) // buffer file contents (?) .pipe(buffer()) // Initialize Source Maps buildStream = buildStream // loads map from browserify file .pipe(sourcemaps.init({ loadMaps: true })) // Minification if (!opts.devMode) { buildStream = buildStream.pipe( terser({ mangle: { reserved: ['MetamaskInpageProvider'], }, sourceMap: { content: true, }, }), ) } // Finalize Source Maps if (opts.devMode) { // Use inline source maps for development due to Chrome DevTools bug // https://bugs.chromium.org/p/chromium/issues/detail?id=931675 buildStream = buildStream.pipe(sourcemaps.write()) } else { buildStream = buildStream.pipe(sourcemaps.write('../sourcemaps')) } // write completed bundles browserPlatforms.forEach((platform) => { const dest = `./dist/${platform}` buildStream = buildStream.pipe(gulp.dest(dest)) }) await endOfStream(buildStream) } } function configureBundleForSesify({ browserifyOpts, bundleName }) { // add in sesify args for better globalRef usage detection Object.assign(browserifyOpts, sesify.args) // ensure browserify uses full paths browserifyOpts.fullPaths = true // record dependencies used in bundle fs.mkdirSync('./sesify', { recursive: true }) browserifyOpts.plugin.push([ 'deps-dump', { filename: `./sesify/deps-${bundleName}.json`, }, ]) const sesifyConfigPath = `./sesify/${bundleName}.json` // add sesify plugin browserifyOpts.plugin.push([ sesify, { writeAutoConfig: sesifyConfigPath, }, ]) // remove html comments that SES is alergic to const removeHtmlComment = makeStringTransform( 'remove-html-comment', { excludeExtension: ['.json'] }, (content, _, cb) => { const result = content.split('-->').join('-- >') cb(null, result) }, ) browserifyOpts.transform.push([removeHtmlComment, { global: true }]) } function generateBundler(opts, performBundle) { const browserifyOpts = assign({}, watchify.args, { plugin: [], transform: [], debug: true, fullPaths: opts.devMode, }) const bundleName = opts.filename.split('.')[0] // activate sesify const activateAutoConfig = Boolean(process.env.SESIFY_AUTOGEN) // const activateSesify = activateAutoConfig const activateSesify = activateAutoConfig && ['background'].includes(bundleName) if (activateSesify) { configureBundleForSesify({ browserifyOpts, bundleName }) } if (!activateSesify) { browserifyOpts.plugin.push('browserify-derequire') } if (!opts.buildLib) { if (opts.devMode && opts.filename === 'ui.js') { browserifyOpts.entries = [ './development/require-react-devtools.js', opts.filepath, ] } else { browserifyOpts.entries = [opts.filepath] } } let bundler = browserify(browserifyOpts) .transform('babelify') // Transpile any dependencies using the object spread/rest operator // because it is incompatible with `esprima`, which is used by `envify` // See https://github.com/jquery/esprima/issues/1927 .transform('babelify', { only: ['./**/node_modules/libp2p'], global: true, plugins: ['@babel/plugin-proposal-object-rest-spread'], }) .transform('brfs') if (opts.buildLib) { bundler = bundler.require(opts.dependenciesToBundle) } if (opts.externalDependencies) { bundler = bundler.external(opts.externalDependencies) } const environment = getEnvironment({ devMode: opts.devMode, test: opts.testing, }) if (environment === 'production' && !process.env.SENTRY_DSN) { throw new Error('Missing SENTRY_DSN environment variable') } // Inject variables into bundle bundler.transform( envify({ METAMASK_DEBUG: opts.devMode, METAMASK_ENVIRONMENT: environment, METAMETRICS_PROJECT_ID: process.env.METAMETRICS_PROJECT_ID, NODE_ENV: opts.devMode ? 'development' : 'production', IN_TEST: opts.testing ? 'true' : false, PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '', PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '', ETH_GAS_STATION_API_KEY: process.env.ETH_GAS_STATION_API_KEY || '', CONF: opts.devMode ? conf : {}, SENTRY_DSN: process.env.SENTRY_DSN, INFURA_PROJECT_ID: opts.testing ? '00000000000000000000000000000000' : conf.INFURA_PROJECT_ID, SEGMENT_HOST: conf.SEGMENT_HOST, // When we're in the 'production' environment we will use a specific key only set in CI // Otherwise we'll use the key from .metamaskrc or from the environment variable. If // the value of SEGMENT_WRITE_KEY that we envify is undefined then no events will be tracked // in the build. This is intentional so that developers can contribute to MetaMask without // inflating event volume. SEGMENT_WRITE_KEY: environment === 'production' ? process.env.SEGMENT_PROD_WRITE_KEY : conf.SEGMENT_WRITE_KEY, SEGMENT_LEGACY_WRITE_KEY: environment === 'production' ? process.env.SEGMENT_PROD_LEGACY_WRITE_KEY : conf.SEGMENT_LEGACY_WRITE_KEY, }), { global: true, }, ) // Live reload - minimal rebundle on change if (opts.devMode) { bundler = watchify(bundler) // on any file update, re-runs the bundler bundler.on('update', () => { performBundle() }) } return bundler } } function beep() { process.stdout.write('\x07') } function getEnvironment({ devMode, test }) { // get environment slug if (devMode) { return 'development' } else if (test) { return 'testing' } else if (process.env.CIRCLE_BRANCH === 'master') { return 'production' } else if ( /^Version-v(\d+)[.](\d+)[.](\d+)/u.test(process.env.CIRCLE_BRANCH) ) { return 'release-candidate' } else if (process.env.CIRCLE_BRANCH === 'develop') { return 'staging' } else if (process.env.CIRCLE_PULL_REQUEST) { return 'pull-request' } return 'other' }