diff --git a/.circleci/scripts/create-sesify-viz b/.circleci/scripts/create-sesify-viz index ddaa04eb1..33dc0bc72 100755 --- a/.circleci/scripts/create-sesify-viz +++ b/.circleci/scripts/create-sesify-viz @@ -9,5 +9,5 @@ set -o pipefail mkdir -p ./build-artifacts/deps-viz/ # generate viz -SESIFY_AUTOGEN=1 npx gulp build:extension:js:background +SESIFY_AUTOGEN=1 yarn build scripts:core:prod:background npx sesify-viz --deps sesify/deps-background.json --config sesify/background.json --dest ./build-artifacts/deps-viz/background \ No newline at end of file diff --git a/app/manifest.json b/app/manifest/_base.json similarity index 93% rename from app/manifest.json rename to app/manifest/_base.json index 58d4e1308..d5156bea7 100644 --- a/app/manifest.json +++ b/app/manifest/_base.json @@ -24,16 +24,9 @@ "128": "images/icon-128.png", "512": "images/icon-512.png" }, - "applications": { - "gecko": { - "id": "webextension@metamask.io", - "strict_min_version": "56.0" - } - }, "default_locale": "en", "background": { "scripts": [ - "chromereload.js", "bg-libs.js", "background.js" ], diff --git a/app/manifest/brave.json b/app/manifest/brave.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/app/manifest/brave.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/app/manifest/chrome.json b/app/manifest/chrome.json new file mode 100644 index 000000000..ba81cab75 --- /dev/null +++ b/app/manifest/chrome.json @@ -0,0 +1,3 @@ +{ + "minimum_chrome_version": "58" +} \ No newline at end of file diff --git a/app/manifest/firefox.json b/app/manifest/firefox.json new file mode 100644 index 000000000..bee5f4e6c --- /dev/null +++ b/app/manifest/firefox.json @@ -0,0 +1,8 @@ +{ + "applications": { + "gecko": { + "id": "webextension@metamask.io", + "strict_min_version": "56.0" + } + } +} \ No newline at end of file diff --git a/app/manifest/opera.json b/app/manifest/opera.json new file mode 100644 index 000000000..37beda645 --- /dev/null +++ b/app/manifest/opera.json @@ -0,0 +1,9 @@ +{ + "permissions": [ + "storage", + "tabs", + "clipboardWrite", + "clipboardRead", + "http://localhost:8545/" + ] +} \ No newline at end of file diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a2ea4abac..e9f9414ca 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -50,7 +50,7 @@ import accountImporter from './account-import-strategies' import getBuyEthUrl from './lib/buy-eth-url' import selectChainId from './lib/select-chain-id' import { Mutex } from 'await-semaphore' -import { version } from '../manifest.json' +import { version } from '../manifest/_base.json' import ethUtil, { BN } from 'ethereumjs-util' const GWEI_BN = new BN('1000000000') diff --git a/development/announcer.js b/development/announcer.js index b4036dd15..798e7da07 100644 --- a/development/announcer.js +++ b/development/announcer.js @@ -1,14 +1,10 @@ -const manifest = require('../app/manifest.json') - -const version = manifest.version - const fs = require('fs') const path = require('path') +const { version } = require('../app/manifest/_base.json') -const changelog = fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md')).toString() +const changelog = fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md'), 'utf8') const log = changelog.split(version)[1].split('##')[0].trim() - const msg = `*MetaMask ${version}* now published! It should auto-update soon!\n${log}` console.log(msg) diff --git a/development/build/display.js b/development/build/display.js new file mode 100644 index 000000000..008b19d0c --- /dev/null +++ b/development/build/display.js @@ -0,0 +1,150 @@ +const randomColor = require('randomcolor') +const chalk = require('chalk') + +module.exports = { setupTaskDisplay, displayChart } + +const SYMBOLS = { + Empty: '', + Space: ' ', + Full: '█', + SevenEighths: '▉', + ThreeQuarters: '▊', + FiveEighths: '▋', + Half: '▌', + ThreeEighths: '▍', + Quarter: '▎', + Eighth: '▏', + RightHalf: '▐', + RightEigth: '▕', +} + +function setupTaskDisplay (taskEvents) { + const taskData = [] + taskEvents.on('start', ([name]) => { + console.log(`Starting '${name}'...`) + }) + taskEvents.on('end', ([name, start, end]) => { + taskData.push([name, start, end]) + console.log(`Finished '${name}'`) + }) + taskEvents.on('complete', () => { + displayChart(taskData) + }) +} + +function displayChart (data) { + // sort tasks by start time + data.sort((a, b,) => a[1] - b[1]) + + // get bounds + const first = Math.min(...data.map((entry) => entry[1])) + const last = Math.max(...data.map((entry) => entry[2])) + + // get colors + const colors = randomColor({ count: data.length }) + + // some heading before the bars + console.log(`\nbuild completed. task timeline:`) + + // build bars for bounds + data.map((entry, index) => { + const [label, start, end] = entry + const [start2, end2] = [start, end].map((value) => adjust(value, first, last, 40)) + const barString = barBuilder(start2, end2) + const color = colors[index] + const coloredBarString = colorize(color, barString) + const duration = ((end - start) / 1e3).toFixed(1) + console.log(coloredBarString, `${label} ${duration}s`) + }) + +} + +function colorize (color, string) { + const colorizer = (typeof chalk[color] === 'function') ? chalk[color] : chalk.hex(color) + return colorizer(string) +} + +// scale number within bounds +function adjust (value, first, last, size) { + const length = last - first + const result = (value - first) / length * size + return result +} + +// draw bars +function barBuilder (start, end) { + const [spaceInt, spaceRest] = splitNumber(start) + const barBodyLength = end - spaceInt + let [barInt, barRest] = splitNumber(barBodyLength) + // We are handling zero value as a special case + // to print at least something on the screen + if (barInt === 0 && barRest === 0) { + barInt = 0 + barRest = 0.001 + } + + const spaceFull = SYMBOLS.Space.repeat(spaceInt) + const spacePartial = getSymbolNormalRight(spaceRest) + const barFull = SYMBOLS.Full.repeat(barInt) + const barPartial = getSymbolNormal(barRest) + + return `${spaceFull}${spacePartial}${barFull}${barPartial}` +} + +// get integer and remainder +function splitNumber (value = 0) { + const [int, rest = '0'] = value.toString().split('.') + const int2 = parseInt(int, 10) + const rest2 = parseInt(rest, 10) / Math.pow(10, rest.length) + return [int2, rest2] +} + +// get partial block char for value (left-adjusted) +function getSymbolNormal (value) { + // round to closest supported value + const possibleValues = [0, 1 / 8, 1 / 4, 3 / 8, 1 / 2, 5 / 8, 3 / 4, 7 / 8, 1] + const rounded = possibleValues.reduce((prev, curr) => { + return (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev) + }) + + if (rounded === 0) { + return SYMBOLS.Empty + } else if (rounded === 1 / 8) { + return SYMBOLS.Eighth + } else if (rounded === 1 / 4) { + return SYMBOLS.Quarter + } else if (rounded === 3 / 8) { + return SYMBOLS.ThreeEighths + } else if (rounded === 1 / 2) { + return SYMBOLS.Half + } else if (rounded === 5 / 8) { + return SYMBOLS.FiveEighths + } else if (rounded === 3 / 4) { + return SYMBOLS.ThreeQuarters + } else if (rounded === 7 / 8) { + return SYMBOLS.SevenEighths + } else { + return SYMBOLS.Full + } +} + +// get partial block char for value (right-adjusted) +function getSymbolNormalRight (value) { + // round to closest supported value (not much :/) + const possibleValues = [0, 1 / 2, 7 / 8, 1] + const rounded = possibleValues.reduce((prev, curr) => { + return (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev) + }) + + if (rounded === 0) { + return SYMBOLS.Full + } else if (rounded === 1 / 2) { + return SYMBOLS.RightHalf + } else if (rounded === 7 / 8) { + return SYMBOLS.RightEigth + } else if (rounded === 1) { + return SYMBOLS.Space + } else { + throw new Error('getSymbolNormalRight got unexpected result') + } +} diff --git a/development/build/etc.js b/development/build/etc.js new file mode 100644 index 000000000..ab0c2afb8 --- /dev/null +++ b/development/build/etc.js @@ -0,0 +1,42 @@ +const gulp = require('gulp') +const gulpZip = require('gulp-zip') +const del = require('del') +const { promises: fs } = require('fs') +const pify = require('pify') +const pump = pify(require('pump')) +const baseManifest = require('../../app/manifest/_base.json') +const { createTask, composeParallel } = require('./task') + +module.exports = createEtcTasks + + +function createEtcTasks ({ browserPlatforms, livereload }) { + + const clean = createTask('clean', async function clean () { + await del(['./dist/*']) + await Promise.all(browserPlatforms.map(async (platform) => { + await fs.mkdir(`./dist/${platform}`, { recursive: true }) + })) + }) + + const reload = createTask('reload', function devReload () { + livereload.listen({ port: 35729 }) + }) + + // zip tasks for distribution + const zip = createTask('zip', composeParallel( + ...browserPlatforms.map((platform) => createZipTask(platform)) + )) + + return { clean, reload, zip } +} + +function createZipTask (target) { + return async () => { + await pump( + gulp.src(`dist/${target}/**`), + gulpZip(`metamask-${target}-${baseManifest.version}.zip`), + gulp.dest('builds'), + ) + } +} diff --git a/development/build/index.js b/development/build/index.js new file mode 100755 index 000000000..5b073de55 --- /dev/null +++ b/development/build/index.js @@ -0,0 +1,91 @@ +// +// build task definitions +// +// run any task with "yarn build ${taskName}" +// + +const livereload = require('gulp-livereload') +const { createTask, composeSeries, composeParallel, detectAndRunEntryTask } = require('./task') +const createManifestTasks = require('./manifest') +const createScriptTasks = require('./scripts') +const createStyleTasks = require('./styles') +const createStaticAssetTasks = require('./static') +const createEtcTasks = require('./etc') + +const browserPlatforms = [ + 'firefox', + 'chrome', + 'brave', + 'opera', +] + +defineAllTasks() +detectAndRunEntryTask() + +function defineAllTasks () { + + const staticTasks = createStaticAssetTasks({ livereload, browserPlatforms }) + const manifestTasks = createManifestTasks({ browserPlatforms }) + const styleTasks = createStyleTasks({ livereload }) + const scriptTasks = createScriptTasks({ livereload, browserPlatforms }) + const { clean, reload, zip } = createEtcTasks({ livereload, browserPlatforms }) + + // build for development (livereload) + createTask('dev', + composeSeries( + clean, + styleTasks.dev, + composeParallel( + scriptTasks.dev, + staticTasks.dev, + manifestTasks.dev, + reload + ) + ) + ) + + // build for test development (livereload) + createTask('testDev', + composeSeries( + clean, + styleTasks.dev, + composeParallel( + scriptTasks.testDev, + staticTasks.dev, + manifestTasks.testDev, + reload + ) + ) + ) + + // build for prod release + createTask('prod', + composeSeries( + clean, + styleTasks.prod, + composeParallel( + scriptTasks.prod, + staticTasks.prod, + manifestTasks.prod, + ), + zip, + ) + ) + + // build for CI testing + createTask('test', + composeSeries( + clean, + styleTasks.prod, + composeParallel( + scriptTasks.test, + staticTasks.prod, + manifestTasks.test, + ), + ) + ) + + // special build for minimal CI testing + createTask('styles', styleTasks.prod) + +} diff --git a/development/build/manifest.js b/development/build/manifest.js new file mode 100644 index 000000000..71d335be0 --- /dev/null +++ b/development/build/manifest.js @@ -0,0 +1,97 @@ +const { promises: fs } = require('fs') +const { merge, cloneDeep } = require('lodash') + +const baseManifest = require('../../app/manifest/_base.json') + +const { createTask, composeSeries } = require('./task') + +module.exports = createManifestTasks + + +const scriptsToExcludeFromBackgroundDevBuild = { + 'bg-libs.js': true, +} + +function createManifestTasks ({ browserPlatforms }) { + + // merge base manifest with per-platform manifests + const prepPlatforms = async () => { + return Promise.all(browserPlatforms.map(async (platform) => { + const platformModifications = await readJson(`${__dirname}/../../app/manifest/${platform}.json`) + const result = merge(cloneDeep(baseManifest), platformModifications) + const dir = `./dist/${platform}` + await fs.mkdir(dir, { recursive: true }) + await writeJson(result, `${dir}/manifest.json`) + })) + } + + // dev: remove bg-libs, add chromereload, add perms + const envDev = createTaskForModifyManifestForEnvironment((manifest) => { + const scripts = manifest.background.scripts.filter((scriptName) => !scriptsToExcludeFromBackgroundDevBuild[scriptName]) + scripts.push('chromereload.js') + manifest.background = { + ...manifest.background, + scripts, + } + manifest.permissions = [...manifest.permissions, 'webRequestBlocking'] + }) + + // testDev: remove bg-libs, add perms + const envTestDev = createTaskForModifyManifestForEnvironment((manifest) => { + const scripts = manifest.background.scripts.filter((scriptName) => !scriptsToExcludeFromBackgroundDevBuild[scriptName]) + scripts.push('chromereload.js') + manifest.background = { + ...manifest.background, + scripts, + } + manifest.permissions = [...manifest.permissions, 'webRequestBlocking', 'http://localhost/*'] + }) + + // test: add permissions + const envTest = createTaskForModifyManifestForEnvironment((manifest) => { + manifest.permissions = [...manifest.permissions, 'webRequestBlocking', 'http://localhost/*'] + }) + + // high level manifest tasks + const dev = createTask('manifest:dev', composeSeries( + prepPlatforms, + envDev, + )) + + const testDev = createTask('manifest:testDev', composeSeries( + prepPlatforms, + envTestDev, + )) + + const test = createTask('manifest:test', composeSeries( + prepPlatforms, + envTest, + )) + + const prod = createTask('manifest:prod', prepPlatforms) + + return { prod, dev, testDev, test } + + // helper for modifying each platform's manifest.json in place + function createTaskForModifyManifestForEnvironment (transformFn) { + return () => { + return Promise.all(browserPlatforms.map(async (platform) => { + const path = `./dist/${platform}/manifest.json` + const manifest = await readJson(path) + transformFn(manifest) + await writeJson(manifest, path) + })) + } + } + +} + +// helper for reading and deserializing json from fs +async function readJson (path) { + return JSON.parse(await fs.readFile(path, 'utf8')) +} + +// helper for serializing and writing json to fs +async function writeJson (obj, path) { + return fs.writeFile(path, JSON.stringify(obj, null, 2)) +} diff --git a/development/build/scripts.js b/development/build/scripts.js new file mode 100644 index 000000000..342281d1c --- /dev/null +++ b/development/build/scripts.js @@ -0,0 +1,362 @@ +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 { createTask, composeParallel, composeSeries, runInChildProcess } = require('./task') +const packageJSON = require('../../package.json') + +module.exports = createScriptTasks + + +const dependencies = Object.keys((packageJSON && packageJSON.dependencies) || {}) +const materialUIDependencies = ['@material-ui/core'] +const reactDepenendencies = dependencies.filter((dep) => dep.match(/react/)) +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 = core.dev + const testDev = core.testDev + + 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' ], + }, + })) + } + + // 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) + } + + let environment + if (opts.devMode) { + environment = 'development' + } else if (opts.testing) { + environment = 'testing' + } else if (process.env.CIRCLE_BRANCH === 'master') { + environment = 'production' + } else if (/^Version-v(\d+)[.](\d+)[.](\d+)/.test(process.env.CIRCLE_BRANCH)) { + environment = 'release-candidate' + } else if (process.env.CIRCLE_BRANCH === 'develop') { + environment = 'staging' + } else if (process.env.CIRCLE_PULL_REQUEST) { + environment = 'pull-request' + } else { + environment = 'other' + } + + // Inject variables into bundle + bundler.transform(envify({ + METAMASK_DEBUG: opts.devMode, + METAMASK_ENVIRONMENT: environment, + 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 || '', + }), { + 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') +} diff --git a/development/build/static.js b/development/build/static.js new file mode 100644 index 000000000..1573279b7 --- /dev/null +++ b/development/build/static.js @@ -0,0 +1,95 @@ +const fs = require('fs-extra') +const path = require('path') +const watch = require('gulp-watch') +const glob = require('fast-glob') + +const { createTask, composeSeries } = require('./task') + +module.exports = createStaticAssetTasks + + +const copyTargets = [ + { + src: `./app/_locales/`, + dest: `_locales`, + }, + { + src: `./app/images/`, + dest: `images`, + }, + { + src: `./node_modules/eth-contract-metadata/images/`, + dest: `images/contract`, + }, + { + src: `./app/fonts/`, + dest: `fonts`, + }, + { + src: `./app/vendor/`, + dest: `vendor`, + }, + { + src: `./ui/app/css/output/`, + pattern: `*.css`, + dest: ``, + }, + { + src: `./app/`, + pattern: `*.html`, + dest: ``, + }, +] + +const copyTargetsDev = [ + ...copyTargets, + { + src: './app/scripts/', + pattern: '/chromereload.js', + dest: ``, + }, +] + +function createStaticAssetTasks ({ livereload, browserPlatforms }) { + + const prod = createTask('static:prod', composeSeries(...copyTargets.map((target) => { + return async function copyStaticAssets () { + await performCopy(target) + } + }))) + const dev = createTask('static:dev', composeSeries(...copyTargetsDev.map((target) => { + return async function copyStaticAssets () { + await setupLiveCopy(target) + } + }))) + + return { dev, prod } + + async function setupLiveCopy (target) { + const pattern = target.pattern || '/**/*' + watch(target.src + pattern, (event) => { + livereload.changed(event.path) + performCopy(target) + }) + await performCopy(target) + } + + async function performCopy (target) { + await Promise.all(browserPlatforms.map(async (platform) => { + if (target.pattern) { + await copyGlob(target.src, `${target.src}${target.pattern}`, `./dist/${platform}/${target.dest}`) + } else { + await copyGlob(target.src, `${target.src}`, `./dist/${platform}/${target.dest}`) + } + })) + } + + async function copyGlob (baseDir, srcGlob, dest) { + const sources = await glob(srcGlob, { onlyFiles: false }) + await Promise.all(sources.map(async (src) => { + const relativePath = path.relative(baseDir, src) + await fs.copy(src, `${dest}${relativePath}`) + })) + } + +} diff --git a/development/build/styles.js b/development/build/styles.js new file mode 100644 index 000000000..4667100c8 --- /dev/null +++ b/development/build/styles.js @@ -0,0 +1,79 @@ +const pify = require('pify') +const gulp = require('gulp') +const sass = require('gulp-sass') +sass.compiler = require('node-sass') +const autoprefixer = require('gulp-autoprefixer') +const gulpStylelint = require('gulp-stylelint') +const watch = require('gulp-watch') +const sourcemaps = require('gulp-sourcemaps') +const rtlcss = require('gulp-rtlcss') +const rename = require('gulp-rename') +const pump = pify(require('pump')) +const { createTask } = require('./task') + + +// scss compilation and autoprefixing tasks +module.exports = createStyleTasks + + +function createStyleTasks ({ livereload }) { + + const prod = createTask('styles:prod', createScssBuildTask({ + src: 'ui/app/css/index.scss', + dest: 'ui/app/css/output', + devMode: false, + })) + + const dev = createTask('styles:dev', createScssBuildTask({ + src: 'ui/app/css/index.scss', + dest: 'ui/app/css/output', + devMode: true, + pattern: 'ui/app/**/*.scss', + })) + + const lint = createTask('lint-scss', function () { + return gulp + .src('ui/app/css/itcss/**/*.scss') + .pipe(gulpStylelint({ + reporters: [ + { formatter: 'string', console: true }, + ], + fix: true, + })) + }) + + return { prod, dev, lint } + + + function createScssBuildTask ({ src, dest, devMode, pattern }) { + return async function () { + if (devMode) { + watch(pattern, async (event) => { + await buildScss(devMode) + livereload.changed(event.path) + }) + } + await buildScss(devMode) + } + + async function buildScss (devMode) { + await pump(...[ + // pre-process + gulp.src(src), + devMode && sourcemaps.init(), + sass().on('error', sass.logError), + devMode && sourcemaps.write(), + autoprefixer(), + // standard + gulp.dest(dest), + // right-to-left + rtlcss(), + rename({ suffix: '-rtl' }), + devMode && sourcemaps.write(), + gulp.dest(dest), + ].filter(Boolean)) + } + } + + +} diff --git a/development/build/task.js b/development/build/task.js new file mode 100644 index 000000000..4a0a9d437 --- /dev/null +++ b/development/build/task.js @@ -0,0 +1,101 @@ +const EventEmitter = require('events') +const { spawn } = require('child_process') + +const tasks = {} +const taskEvents = new EventEmitter() + +module.exports = { detectAndRunEntryTask, tasks, taskEvents, createTask, runTask, composeSeries, composeParallel, runInChildProcess } + +const { setupTaskDisplay } = require('./display') + + +function detectAndRunEntryTask () { + // get requested task name and execute + const taskName = process.argv[2] + if (!taskName) { + throw new Error(`MetaMask build: No task name specified`) + } + const skipStats = process.argv[3] === '--skip-stats' + + runTask(taskName, { skipStats }) +} + +async function runTask (taskName, { skipStats } = {}) { + if (!(taskName in tasks)) { + throw new Error(`MetaMask build: Unrecognized task name "${taskName}"`) + } + if (!skipStats) { + setupTaskDisplay(taskEvents) + console.log(`running task "${taskName}"...`) + } + try { + await tasks[taskName]() + } catch (err) { + console.error(`MetaMask build: Encountered an error while running task "${taskName}".`) + console.error(err) + process.exit(1) + } + taskEvents.emit('complete') +} + +function createTask (taskName, taskFn) { + if (taskName in tasks) { + throw new Error(`MetaMask build: task "${taskName}" already exists. Refusing to redefine`) + } + const task = instrumentForTaskStats(taskName, taskFn) + task.taskName = taskName + tasks[taskName] = task + return task +} + +function runInChildProcess (task) { + const taskName = typeof task === 'string' ? task : task.taskName + if (!taskName) { + throw new Error(`MetaMask build: runInChildProcess unable to identify task name`) + } + return instrumentForTaskStats(taskName, async () => { + const childProcess = spawn('yarn', ['build', taskName, '--skip-stats']) + // forward logs to main process + // skip the first stdout event (announcing the process command) + childProcess.stdout.once('data', () => { + childProcess.stdout.on('data', (data) => process.stdout.write(`${taskName}: ${data}`)) + }) + childProcess.stderr.on('data', (data) => process.stderr.write(`${taskName}: ${data}`)) + // await end of process + await new Promise((resolve, reject) => { + childProcess.once('close', (errCode) => { + if (errCode !== 0) { + reject(new Error(`MetaMask build: runInChildProcess for task "${taskName}" encountered an error`)) + return + } + resolve() + }) + }) + }) +} + +function instrumentForTaskStats (taskName, asyncFn) { + return async () => { + const start = Date.now() + taskEvents.emit('start', [taskName, start]) + await asyncFn() + const end = Date.now() + taskEvents.emit('end', [taskName, start, end]) + } +} + +function composeSeries (...subtasks) { + return async () => { + const realTasks = subtasks + for (const subtask of realTasks) { + await subtask() + } + } +} + +function composeParallel (...subtasks) { + return async () => { + const realTasks = subtasks + await Promise.all(realTasks.map((subtask) => subtask())) + } +} diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index ca020a0bf..000000000 --- a/gulpfile.js +++ /dev/null @@ -1,724 +0,0 @@ -const fs = require('fs') -const watchify = require('watchify') -const browserify = require('browserify') -const envify = require('envify/custom') -const gulp = require('gulp') -const source = require('vinyl-source-stream') -const buffer = require('vinyl-buffer') -const log = require('fancy-log') -const watch = require('gulp-watch') -const sourcemaps = require('gulp-sourcemaps') -const jsoneditor = require('gulp-json-editor') -const zip = require('gulp-zip') -const { assign } = require('lodash') -const livereload = require('gulp-livereload') -const del = require('del') -const manifest = require('./app/manifest.json') -const sass = require('gulp-sass') -const autoprefixer = require('gulp-autoprefixer') -const gulpStylelint = require('gulp-stylelint') -const terser = require('gulp-terser-js') -const pify = require('pify') -const rtlcss = require('gulp-rtlcss') -const rename = require('gulp-rename') -const gulpMultiProcess = require('gulp-multi-process') -const endOfStream = pify(require('end-of-stream')) -const sesify = require('sesify') -const imagemin = require('gulp-imagemin') -const { makeStringTransform } = require('browserify-transform-tools') - -const packageJSON = require('./package.json') - -sass.compiler = require('node-sass') - -const dependencies = Object.keys((packageJSON && packageJSON.dependencies) || {}) -const materialUIDependencies = ['@material-ui/core'] -const reactDepenendencies = dependencies.filter((dep) => dep.match(/react/)) -const d3Dependencies = ['c3', 'd3'] - -const externalDependenciesMap = { - background: [ - '3box', - ], - ui: [ - ...materialUIDependencies, ...reactDepenendencies, ...d3Dependencies, - ], -} - -function gulpParallel (...args) { - return function spawnGulpChildProcess (cb) { - return gulpMultiProcess(args, cb, true) - } -} - -const browserPlatforms = [ - 'firefox', - 'chrome', - 'brave', - 'opera', -] -const commonPlatforms = [ - // browser extensions - ...browserPlatforms, -] - -// browser reload - -gulp.task('dev:reload', function () { - livereload.listen({ - port: 35729, - }) -}) - -// copy universal - -const copyTaskNames = [] -const copyDevTaskNames = [] - -createCopyTasks('locales', { - source: './app/_locales/', - destinations: commonPlatforms.map((platform) => `./dist/${platform}/_locales`), -}) -createCopyTasks('images', { - source: './app/images/', - destinations: commonPlatforms.map((platform) => `./dist/${platform}/images`), -}) -createCopyTasks('contractImages', { - source: './node_modules/eth-contract-metadata/images/', - destinations: commonPlatforms.map((platform) => `./dist/${platform}/images/contract`), -}) -createCopyTasks('fonts', { - source: './app/fonts/', - destinations: commonPlatforms.map((platform) => `./dist/${platform}/fonts`), -}) -createCopyTasks('vendor', { - source: './app/vendor/', - destinations: commonPlatforms.map((platform) => `./dist/${platform}/vendor`), -}) -createCopyTasks('css', { - source: './ui/app/css/output/', - destinations: commonPlatforms.map((platform) => `./dist/${platform}`), -}) -createCopyTasks('reload', { - devOnly: true, - source: './app/scripts/', - pattern: '/chromereload.js', - destinations: commonPlatforms.map((platform) => `./dist/${platform}`), -}) -createCopyTasks('html', { - source: './app/', - pattern: '/*.html', - destinations: commonPlatforms.map((platform) => `./dist/${platform}`), -}) - -// copy extension - -createCopyTasks('manifest', { - source: './app/', - pattern: '/*.json', - destinations: browserPlatforms.map((platform) => `./dist/${platform}`), -}) - -function createCopyTasks (label, opts) { - if (!opts.devOnly) { - const copyTaskName = `copy:${label}` - copyTask(copyTaskName, opts) - copyTaskNames.push(copyTaskName) - } - const copyDevTaskName = `dev:copy:${label}` - copyTask(copyDevTaskName, Object.assign({ devMode: true }, opts)) - copyDevTaskNames.push(copyDevTaskName) -} - -function copyTask (taskName, opts) { - const source = opts.source - const destination = opts.destination - const destinations = opts.destinations || [destination] - const pattern = opts.pattern || '/**/*' - const devMode = opts.devMode - - return gulp.task(taskName, function () { - if (devMode) { - watch(source + pattern, (event) => { - livereload.changed(event.path) - performCopy() - }) - } - - return performCopy() - }) - - function performCopy () { - // stream from source - let stream = gulp.src(source + pattern, { base: source }) - - // copy to destinations - destinations.forEach(function (destination) { - stream = stream.pipe(gulp.dest(destination)) - }) - - return stream - } -} - -// manifest tinkering - -gulp.task('manifest:chrome', function () { - return gulp.src('./dist/chrome/manifest.json') - .pipe(jsoneditor(function (json) { - delete json.applications - json.minimum_chrome_version = '58' - return json - })) - .pipe(gulp.dest('./dist/chrome', { overwrite: true })) -}) - -gulp.task('manifest:opera', function () { - return gulp.src('./dist/opera/manifest.json') - .pipe(jsoneditor(function (json) { - json.permissions = [ - 'storage', - 'tabs', - 'clipboardWrite', - 'clipboardRead', - 'http://localhost:8545/', - ] - return json - })) - .pipe(gulp.dest('./dist/opera', { overwrite: true })) -}) - -gulp.task('manifest:production', function () { - return gulp.src([ - './dist/firefox/manifest.json', - './dist/chrome/manifest.json', - './dist/brave/manifest.json', - './dist/opera/manifest.json', - ], { base: './dist/' }) - - // Exclude chromereload script in production: - .pipe(jsoneditor(function (json) { - json.background.scripts = json.background.scripts.filter((script) => { - return !script.includes('chromereload') - }) - return json - })) - - .pipe(gulp.dest('./dist/', { overwrite: true })) -}) - -gulp.task('manifest:testing', function () { - return gulp.src([ - './dist/firefox/manifest.json', - './dist/chrome/manifest.json', - ], { base: './dist/' }) - - // Exclude chromereload script in production: - .pipe(jsoneditor(function (json) { - json.permissions = [...json.permissions, 'webRequestBlocking', 'http://localhost/*'] - return json - })) - - .pipe(gulp.dest('./dist/', { overwrite: true })) -}) - -const scriptsToExcludeFromBackgroundDevBuild = { - 'bg-libs.js': true, -} - -gulp.task('manifest:testing-local', function () { - return gulp.src([ - './dist/firefox/manifest.json', - './dist/chrome/manifest.json', - ], { base: './dist/' }) - - .pipe(jsoneditor(function (json) { - json.background = { - ...json.background, - scripts: json.background.scripts.filter((scriptName) => !scriptsToExcludeFromBackgroundDevBuild[scriptName]), - } - json.permissions = [...json.permissions, 'webRequestBlocking', 'http://localhost/*'] - return json - })) - - .pipe(gulp.dest('./dist/', { overwrite: true })) -}) - - -gulp.task('manifest:dev', function () { - return gulp.src([ - './dist/firefox/manifest.json', - './dist/chrome/manifest.json', - ], { base: './dist/' }) - - .pipe(jsoneditor(function (json) { - json.background = { - ...json.background, - scripts: json.background.scripts.filter((scriptName) => !scriptsToExcludeFromBackgroundDevBuild[scriptName]), - } - json.permissions = [...json.permissions, 'webRequestBlocking'] - return json - })) - - .pipe(gulp.dest('./dist/', { overwrite: true })) -}) - -gulp.task('optimize:images', function () { - return gulp.src('./dist/**/images/**', { base: './dist/' }) - .pipe(imagemin()) - .pipe(gulp.dest('./dist/', { overwrite: true })) -}) - -gulp.task('copy', - gulp.series( - gulp.parallel(...copyTaskNames), - 'manifest:production', - 'manifest:chrome', - 'manifest:opera' - ) -) - -gulp.task('dev:copy', - gulp.series( - gulp.parallel(...copyDevTaskNames), - 'manifest:dev', - 'manifest:chrome', - 'manifest:opera' - ) -) - -gulp.task('test:copy', - gulp.series( - gulp.parallel(...copyDevTaskNames), - 'manifest:chrome', - 'manifest:opera', - 'manifest:testing-local' - ) -) - -// scss compilation and autoprefixing tasks - -gulp.task('build:scss', createScssBuildTask({ - src: 'ui/app/css/index.scss', - dest: 'ui/app/css/output', - devMode: false, -})) - -gulp.task('dev:scss', createScssBuildTask({ - src: 'ui/app/css/index.scss', - dest: 'ui/app/css/output', - devMode: true, - pattern: 'ui/app/**/*.scss', -})) - -function createScssBuildTask ({ src, dest, devMode, pattern }) { - return function () { - if (devMode) { - watch(pattern, async (event) => { - const stream = buildScss() - await endOfStream(stream) - livereload.changed(event.path) - }) - return buildScssWithSourceMaps() - } - return buildScss() - } - - function buildScssWithSourceMaps () { - return gulp.src(src) - .pipe(sourcemaps.init()) - .pipe(sass().on('error', sass.logError)) - .pipe(sourcemaps.write()) - .pipe(autoprefixer()) - .pipe(gulp.dest(dest)) - .pipe(rtlcss()) - .pipe(rename({ suffix: '-rtl' })) - .pipe(sourcemaps.write()) - .pipe(gulp.dest(dest)) - } - - function buildScss () { - return gulp.src(src) - .pipe(sass().on('error', sass.logError)) - .pipe(autoprefixer()) - .pipe(gulp.dest(dest)) - .pipe(rtlcss()) - .pipe(rename({ suffix: '-rtl' })) - .pipe(gulp.dest(dest)) - } -} - -gulp.task('lint-scss', function () { - return gulp - .src('ui/app/css/itcss/**/*.scss') - .pipe(gulpStylelint({ - reporters: [ - { formatter: 'string', console: true }, - ], - fix: true, - })) -}) - -// build js - -const buildJsFiles = [ - 'inpage', - 'contentscript', - 'background', - 'ui', - 'phishing-detect', -] - -// bundle tasks -createTasksForBuildJsDeps({ filename: 'bg-libs', key: 'background' }) -createTasksForBuildJsDeps({ filename: 'ui-libs', key: 'ui' }) -createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'dev:extension:js', devMode: true }) -createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'dev:test-extension:js', devMode: true, testing: 'true' }) -createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'build:extension:js' }) -createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'build:test:extension:js', testing: 'true' }) - -function createTasksForBuildJsDeps ({ key, filename }) { - const destinations = browserPlatforms.map((platform) => `./dist/${platform}`) - - const bundleTaskOpts = Object.assign({ - buildSourceMaps: true, - sourceMapDir: '../sourcemaps', - minifyBuild: true, - devMode: false, - }) - - gulp.task(`build:extension:js:deps:${key}`, bundleTask(Object.assign({ - label: filename, - filename: `${filename}.js`, - destinations, - buildLib: true, - dependenciesToBundle: externalDependenciesMap[key], - }, bundleTaskOpts))) -} - - -function createTasksForBuildJsExtension ({ buildJsFiles, taskPrefix, devMode, testing, bundleTaskOpts = {} }) { - // inpage must be built before all other scripts: - const rootDir = './app/scripts' - const nonInpageFiles = buildJsFiles.filter((file) => file !== 'inpage') - const buildPhase1 = ['inpage'] - const buildPhase2 = nonInpageFiles - const destinations = browserPlatforms.map((platform) => `./dist/${platform}`) - bundleTaskOpts = Object.assign({ - buildSourceMaps: true, - sourceMapDir: '../sourcemaps', - minifyBuild: !devMode, - buildWithFullPaths: devMode, - watch: devMode, - devMode, - testing, - }, bundleTaskOpts) - createTasksForBuildJs({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1, buildPhase2 }) -} - -function createTasksForBuildJs ({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1 = [], buildPhase2 = [] }) { - // bundle task for each file - const jsFiles = [].concat(buildPhase1, buildPhase2) - jsFiles.forEach((jsFile) => { - gulp.task(`${taskPrefix}:${jsFile}`, bundleTask(Object.assign({ - label: jsFile, - filename: `${jsFile}.js`, - filepath: `${rootDir}/${jsFile}.js`, - externalDependencies: bundleTaskOpts.devMode ? undefined : externalDependenciesMap[jsFile], - destinations, - }, bundleTaskOpts))) - }) - // compose into larger task - const subtasks = [] - subtasks.push(gulp.parallel(buildPhase1.map((file) => `${taskPrefix}:${file}`))) - if (buildPhase2.length) { - subtasks.push(gulp.parallel(buildPhase2.map((file) => `${taskPrefix}:${file}`))) - } - - gulp.task(taskPrefix, gulp.series(subtasks)) -} - -// clean dist - -gulp.task('clean', function clean () { - return del(['./dist/*']) -}) - -// zip tasks for distribution -gulp.task('zip:chrome', zipTask('chrome')) -gulp.task('zip:firefox', zipTask('firefox')) -gulp.task('zip:opera', zipTask('opera')) -gulp.task('zip', gulp.parallel('zip:chrome', 'zip:firefox', 'zip:opera')) - -// high level tasks - -gulp.task('dev:test', - gulp.series( - 'clean', - 'dev:scss', - gulp.parallel( - 'dev:test-extension:js', - 'test:copy', - 'dev:reload' - ) - ) -) - -gulp.task('dev:extension', - gulp.series( - 'clean', - 'dev:scss', - gulp.parallel( - 'dev:extension:js', - 'dev:copy', - 'dev:reload' - ) - ) -) - -gulp.task('build', - gulp.series( - 'clean', - 'build:scss', - gulpParallel( - 'build:extension:js:deps:background', - 'build:extension:js:deps:ui', - 'build:extension:js', - 'copy' - ), - 'optimize:images' - ) -) - -gulp.task('build:test', - gulp.series( - 'clean', - 'build:scss', - gulpParallel( - 'build:extension:js:deps:background', - 'build:extension:js:deps:ui', - 'build:test:extension:js', - 'copy' - ), - 'manifest:testing' - ) -) - -gulp.task('dist', - gulp.series( - 'build', - 'zip' - ) -) - -// task generators - -function zipTask (target) { - return () => { - return gulp.src(`dist/${target}/**`) - .pipe(zip(`metamask-${target}-${manifest.version}.zip`)) - .pipe(gulp.dest('builds')) - } -} - -function generateBundler (opts, performBundle) { - const browserifyOpts = assign({}, watchify.args, { - plugin: [], - transform: [], - debug: opts.buildSourceMaps, - fullPaths: opts.buildWithFullPaths, - }) - - 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) - } - - let environment - if (opts.devMode) { - environment = 'development' - } else if (opts.testing) { - environment = 'testing' - } else if (process.env.CIRCLE_BRANCH === 'master') { - environment = 'production' - } else if (/^Version-v(\d+)[.](\d+)[.](\d+)/.test(process.env.CIRCLE_BRANCH)) { - environment = 'release-candidate' - } else if (process.env.CIRCLE_BRANCH === 'develop') { - environment = 'staging' - } else if (process.env.CIRCLE_PULL_REQUEST) { - environment = 'pull-request' - } else { - environment = 'other' - } - - // Inject variables into bundle - bundler.transform(envify({ - METAMASK_DEBUG: opts.devMode, - METAMASK_ENVIRONMENT: environment, - NODE_ENV: opts.devMode ? 'development' : 'production', - IN_TEST: opts.testing, - PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '', - PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '', - }), { - global: true, - }) - - if (opts.watch) { - bundler = watchify(bundler) - // on any file update, re-runs the bundler - bundler.on('update', async (ids) => { - const stream = performBundle() - await endOfStream(stream) - livereload.changed(`${ids}`) - }) - } - - return bundler -} - -function bundleTask (opts) { - let bundler - - return performBundle - - 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.watch) { - 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 - if (opts.buildSourceMaps) { - buildStream = buildStream - // loads map from browserify file - .pipe(sourcemaps.init({ loadMaps: true })) - } - - // Minification - if (opts.minifyBuild) { - buildStream = buildStream - .pipe(terser({ - mangle: { - reserved: [ 'MetamaskInpageProvider' ], - }, - })) - } - - // Finalize Source Maps - if (opts.buildSourceMaps) { - 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(opts.sourceMapDir)) - } - } - - // write completed bundles - opts.destinations.forEach((dest) => { - buildStream = buildStream.pipe(gulp.dest(dest)) - }) - - return 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 beep () { - process.stdout.write('\x07') -} diff --git a/package.json b/package.json index f18a18b34..90600a71e 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,13 @@ "version": "0.0.0", "private": true, "scripts": { - "start": "gulp dev:extension", - "dist": "gulp dist", - "start:test": "gulp dev:test", + "start": "yarn build dev", + "dist": "yarn build prod", + "build": "node development/build/index.js", + "start:test": "yarn build testDev", "benchmark:chrome": "SELENIUM_BROWSER=chrome node test/e2e/benchmark.js", "benchmark:firefox": "SELENIUM_BROWSER=firefox node test/e2e/benchmark.js", - "build:test": "gulp build:test", + "build:test": "yarn build test", "test": "yarn test:unit && yarn lint", "dapp": "node development/static-server.js test/e2e/contract-test --port 8080", "dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && node development/static-server.js test/e2e/contract-test --port 8080'", @@ -19,7 +20,7 @@ "test:unit": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\"", "test:unit:global": "mocha --exit --require test/env.js --require test/setup.js --recursive mocha test/unit-global/*", "test:integration": "yarn test:integration:build && yarn test:flat", - "test:integration:build": "gulp build:scss", + "test:integration:build": "yarn build styles", "test:e2e:chrome": "SELENIUM_BROWSER=chrome test/e2e/run-all.sh", "test:web3:chrome": "SELENIUM_BROWSER=chrome test/e2e/run-web3.sh", "test:web3:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-web3.sh", @@ -193,7 +194,7 @@ "browserify-derequire": "^1.0.1", "browserify-transform-tools": "^1.7.0", "chai": "^4.1.0", - "chalk": "^2.4.2", + "chalk": "^3.0.0", "chromedriver": "^79.0.0", "concurrently": "^5.1.0", "coveralls": "^3.0.0", @@ -210,8 +211,10 @@ "eslint-plugin-mocha": "^6.2.2", "eslint-plugin-react": "^7.18.3", "fancy-log": "^1.3.3", + "fast-glob": "^3.2.2", "fetch-mock": "^6.5.2", "file-loader": "^1.1.11", + "fs-extra": "^8.1.0", "ganache-cli": "^6.4.4", "ganache-core": "2.8.0", "geckodriver": "^1.19.1", @@ -221,7 +224,6 @@ "gulp-babel": "^8.0.0", "gulp-debug": "^3.2.0", "gulp-imagemin": "^6.1.0", - "gulp-json-editor": "^2.5.4", "gulp-livereload": "4.0.0", "gulp-multi-process": "^1.3.1", "gulp-rename": "^1.4.0", @@ -252,6 +254,7 @@ "proxyquire": "^2.1.3", "qs": "^6.2.0", "qunitjs": "^2.4.1", + "randomcolor": "^0.5.4", "react-devtools": "^4.4.0", "react-test-renderer": "^16.12.0", "read-installed": "^4.0.3", diff --git a/test/e2e/fixture-server.js b/test/e2e/fixture-server.js index df13d050c..608955f76 100644 --- a/test/e2e/fixture-server.js +++ b/test/e2e/fixture-server.js @@ -1,4 +1,4 @@ -const fs = require('fs').promises +const { promises: fs } = require('fs') const Koa = require('koa') const path = require('path') diff --git a/yarn.lock b/yarn.lock index 97b1a54cd..7ab987cef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1888,11 +1888,24 @@ "@nodelib/fs.stat" "2.0.2" run-parallel "^1.1.9" +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + "@nodelib/fs.stat@2.0.2", "@nodelib/fs.stat@^2.0.1": version "2.0.2" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.2.tgz#2762aea8fe78ea256860182dcb52d61ee4b8fda6" integrity sha512-z8+wGWV2dgUhLqrtRYa03yDx4HWMvXKi1z8g3m2JyxAx8F7xk74asqPk5LAETjqDSGLFML/6CDl0+yFunSYicw== +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + "@nodelib/fs.stat@^1.1.2": version "1.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" @@ -1906,6 +1919,14 @@ "@nodelib/fs.scandir" "2.1.2" fastq "^1.6.0" +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + dependencies: + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + "@protobufjs/utf8@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" @@ -7265,7 +7286,7 @@ concurrently@^5.1.0: tree-kill "^1.2.2" yargs "^13.3.0" -config-chain@^1.1.11, config-chain@^1.1.12: +config-chain@^1.1.11: version "1.1.12" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== @@ -8484,11 +8505,6 @@ deepmerge@^2.0.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== -deepmerge@^4.2.1: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== - default-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f" @@ -8739,11 +8755,6 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" -detect-indent@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" - integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA== - detect-libc@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -9255,16 +9266,6 @@ editions@^1.3.3: resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b" integrity sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg== -editorconfig@^0.15.3: - version "0.15.3" - resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" - integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g== - dependencies: - commander "^2.19.0" - lru-cache "^4.1.5" - semver "^5.6.0" - sigmund "^1.0.1" - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -11534,6 +11535,18 @@ fast-glob@^3.0.3: merge2 "^1.2.3" micromatch "^4.0.2" +fast-glob@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" + integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + fast-json-parse@^1.0.0, fast-json-parse@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/fast-json-parse/-/fast-json-parse-1.0.3.tgz#43e5c61ee4efa9265633046b770fb682a7577c4d" @@ -12306,6 +12319,15 @@ fs-extra@^8.0.1: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-minipass@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" @@ -12762,7 +12784,7 @@ glob-parent@^3.0.1, glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.0.0, glob-parent@~5.1.0: +glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== @@ -13158,7 +13180,7 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.15: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== -graceful-fs@^4.2.2: +graceful-fs@^4.2.0, graceful-fs@^4.2.2: version "4.2.3" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== @@ -13299,17 +13321,6 @@ gulp-imagemin@^6.1.0: imagemin-optipng "^7.0.0" imagemin-svgo "^7.0.0" -gulp-json-editor@^2.5.4: - version "2.5.4" - resolved "https://registry.yarnpkg.com/gulp-json-editor/-/gulp-json-editor-2.5.4.tgz#b77b46bca22d2dd1ac9f15bbec1eddbe5ef3567c" - integrity sha512-3IdMYsSACfLFYipet9Rmpag7PEU059KnR6TWgfuAfz+ftyzN8yaEvf9vXAD5b9K9v711Ymcpqe6vWGQYfQJ/uQ== - dependencies: - deepmerge "^4.2.1" - detect-indent "^6.0.0" - js-beautify "^1.10.2" - plugin-error "^1.0.1" - through2 "^3.0.1" - gulp-livereload@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/gulp-livereload/-/gulp-livereload-4.0.0.tgz#be4a6b01731a93f76f4fb29c9e62e323affe7d03" @@ -16412,17 +16423,6 @@ js-base64@^2.1.8: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582" integrity sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw== -js-beautify@^1.10.2: - version "1.10.3" - resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.10.3.tgz#c73fa10cf69d3dfa52d8ed624f23c64c0a6a94c1" - integrity sha512-wfk/IAWobz1TfApSdivH5PJ0miIHgDoYb1ugSqHcODPmaYu46rYe5FVuIEkhjg8IQiv6rDNPyhsqbsohI/C2vQ== - dependencies: - config-chain "^1.1.12" - editorconfig "^0.15.3" - glob "^7.1.3" - mkdirp "~0.5.1" - nopt "~4.0.1" - js-levenshtein@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" @@ -18533,14 +18533,6 @@ lru-cache@^3.2.0: dependencies: pseudomap "^1.0.1" -lru-cache@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -18916,6 +18908,11 @@ merge2@^1.2.3: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5" integrity sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA== +merge2@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" + integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== + merkle-lib@^2.0.10: version "2.0.10" resolved "https://registry.yarnpkg.com/merkle-lib/-/merkle-lib-2.0.10.tgz#82b8dbae75e27a7785388b73f9d7725d0f6f3326" @@ -20097,7 +20094,7 @@ nonce-tracker@^1.0.0: dependencies: abbrev "1" -nopt@^4.0.1, nopt@~4.0.1: +nopt@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= @@ -21682,7 +21679,7 @@ phantomjs-prebuilt@^2.1.16, phantomjs-prebuilt@~2.1.7: request-progress "^2.0.1" which "^1.2.10" -picomatch@^2.0.4, picomatch@^2.0.5: +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== @@ -23005,6 +23002,11 @@ randombytes@^2.0.1, randombytes@^2.0.3, randombytes@^2.0.6: dependencies: safe-buffer "^5.1.0" +randomcolor@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/randomcolor/-/randomcolor-0.5.4.tgz#df615b13f25b89ea58c5f8f72647f0a6f07adcc3" + integrity sha512-nYd4nmTuuwMFzHL6W+UWR5fNERGZeVauho8mrJDUSXdNDbao4rbrUwhuLgKC/j8VCS5+34Ria8CsTDuBjrIrQA== + randomfill@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62" @@ -25419,11 +25421,6 @@ side-channel@^1.0.2: es-abstract "^1.17.0-next.1" object-inspect "^1.7.0" -sigmund@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= - signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"