1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-01 21:57:06 +01:00
metamask-extension/development/build/scripts.js
Etienne Dusseault 9f6fa64d67
Add SES lockdown to extension webapp (#9729)
* Freezeglobals: remove Promise freezing, add lockdown

* background & UI: temp disable sentry

* add loose-envify, dedupe symbol-observable

* use loose envify

* add symbol-observable patch

* run freezeGlobals after sentry init

* use require instead of import

* add lockdown to contentscript

* add error code in message

* try increasing node env heap size to 2048

* change back circe CI option

* make freezeGlobals an exported function

* make freezeGlobals an exported function

* use freezeIntrinsics

* pass down env to child process

* fix unknown module

* fix tests

* change back to 2048

* fix import error

* attempt to fix memory error

* fix lint

* fix lint

* fix mem gain

* use lockdown in phishing detect

* fix lint

* move sentry init into freezeIntrinsics to run lockdown before other imports

* lint fix

* custom lockdown modules per context

* lint fix

* fix global test

* remove run in child process

* remove lavamoat-core, use ses, require lockdown directly

* revert childprocess

* patch package postinstall

* revert back child process

* add postinstall to ci

* revert node max space size to 1024

* put back loose-envify

* Disable sentry to see if e2e tetss pass

* use runLockdown, add as script in manifest

* remove global and require from runlockdown

* add more memory to tests

* upgrade resource class for prep-build & prep-build-test

* fix lint

* lint fix

* upgrade remote-redux-devtools

* skillfully re-add sentry

* lintfix

* fix lint

* put back beep

* remove envify, add loose-envify and patch-package in dev deps

* Replace patch with Yarn resolution (#9923)

Instead of patching `symbol-observable`, this ensures that all
versions of `symbol-observable` are resolved to the given range, even
if it contradicts the requested range.

Co-authored-by: Mark Stacey <markjstacey@gmail.com>
2020-11-24 11:26:43 +08:00

441 lines
13 KiB
JavaScript

const fs = require('fs')
const gulp = require('gulp')
const watch = require('gulp-watch')
const pify = require('pify')
const pump = pify(require('pump'))
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('loose-envify/custom')
const sourcemaps = require('gulp-sourcemaps')
const sesify = require('sesify')
const terser = require('gulp-terser-js')
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 baseManifest = require('../../app/manifest/_base.json')
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',
'initSentry',
]
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)
}
const buildPipeline = [
bundler.bundle(),
// convert bundle stream to gulp vinyl stream
source(opts.filename),
// Initialize Source Maps
buffer(),
// loads map from browserify file
sourcemaps.init({ loadMaps: true }),
]
// Minification
if (!opts.devMode) {
buildPipeline.push(
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
// note: sourcemaps call arity is important
buildPipeline.push(sourcemaps.write())
} else {
buildPipeline.push(sourcemaps.write('../sourcemaps'))
}
// write completed bundles
browserPlatforms.forEach((platform) => {
const dest = `./dist/${platform}`
buildPipeline.push(gulp.dest(dest))
})
// process bundles
if (opts.devMode) {
try {
await pump(buildPipeline)
} catch (err) {
gracefulError(err)
}
} else {
await pump(buildPipeline)
}
}
}
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,
METAMASK_VERSION: baseManifest.version,
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 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'
}
function beep() {
process.stdout.write('\x07')
}
function gracefulError(err) {
console.warn(err)
beep()
}