mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-23 02:10:12 +01:00
1276 lines
39 KiB
JavaScript
1276 lines
39 KiB
JavaScript
// TODO(ritave): Remove switches on hardcoded build types
|
|
|
|
const { callbackify } = require('util');
|
|
const path = require('path');
|
|
const { writeFileSync, readFileSync } = require('fs');
|
|
const EventEmitter = require('events');
|
|
const assert = require('assert');
|
|
const gulp = require('gulp');
|
|
const watch = require('gulp-watch');
|
|
const Vinyl = require('vinyl');
|
|
const source = require('vinyl-source-stream');
|
|
const buffer = require('vinyl-buffer');
|
|
const log = require('fancy-log');
|
|
const browserify = require('browserify');
|
|
const watchify = require('watchify');
|
|
const babelify = require('babelify');
|
|
const brfs = require('brfs');
|
|
const envify = require('loose-envify/custom');
|
|
const sourcemaps = require('gulp-sourcemaps');
|
|
const applySourceMap = require('vinyl-sourcemaps-apply');
|
|
const pify = require('pify');
|
|
const through = require('through2');
|
|
const endOfStream = pify(require('end-of-stream'));
|
|
const labeledStreamSplicer = require('labeled-stream-splicer').obj;
|
|
const wrapInStream = require('pumpify').obj;
|
|
const Sqrl = require('squirrelly');
|
|
const lavapack = require('@lavamoat/lavapack');
|
|
const lavamoatBrowserify = require('lavamoat-browserify');
|
|
const terser = require('terser');
|
|
const moduleResolver = require('babel-plugin-module-resolver');
|
|
|
|
const bifyModuleGroups = require('bify-module-groups');
|
|
|
|
const phishingWarningManifest = require('@metamask/phishing-warning/package.json');
|
|
const { streamFlatMap } = require('../stream-flat-map');
|
|
const { generateIconNames } = require('../generate-icon-names');
|
|
const { BUILD_TARGETS, ENVIRONMENT } = require('./constants');
|
|
const { getConfig } = require('./config');
|
|
const {
|
|
isDevBuild,
|
|
isTestBuild,
|
|
getEnvironment,
|
|
logError,
|
|
wrapAgainstScuttling,
|
|
} = require('./utils');
|
|
|
|
const {
|
|
createTask,
|
|
composeParallel,
|
|
composeSeries,
|
|
runInChildProcess,
|
|
} = require('./task');
|
|
const {
|
|
createRemoveFencedCodeTransform,
|
|
} = require('./transforms/remove-fenced-code');
|
|
|
|
// map dist files to bag of needed native APIs against LM scuttling
|
|
const scuttlingConfigBase = {
|
|
'sentry-install.js': {
|
|
// globals sentry need to function
|
|
window: '',
|
|
navigator: '',
|
|
location: '',
|
|
Uint16Array: '',
|
|
fetch: '',
|
|
String: '',
|
|
Math: '',
|
|
Object: '',
|
|
Symbol: '',
|
|
Function: '',
|
|
Array: '',
|
|
Boolean: '',
|
|
Number: '',
|
|
Request: '',
|
|
Date: '',
|
|
JSON: '',
|
|
encodeURIComponent: '',
|
|
console: '',
|
|
crypto: '',
|
|
// {clear/set}Timeout are "this sensitive"
|
|
clearTimeout: 'window',
|
|
setTimeout: 'window',
|
|
// sentry special props
|
|
__SENTRY__: '',
|
|
sentryHooks: '',
|
|
sentry: '',
|
|
appState: '',
|
|
extra: '',
|
|
stateHooks: '',
|
|
},
|
|
};
|
|
|
|
const mv3ScuttlingConfig = { ...scuttlingConfigBase };
|
|
|
|
const standardScuttlingConfig = {
|
|
...scuttlingConfigBase,
|
|
'sentry-install.js': {
|
|
...scuttlingConfigBase['sentry-install.js'],
|
|
document: '',
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Get the appropriate Infura project ID.
|
|
*
|
|
* @param {object} options - The Infura project ID options.
|
|
* @param {string} options.buildType - The current build type.
|
|
* @param {ENVIRONMENT[keyof ENVIRONMENT]} options.environment - The build environment.
|
|
* @param {boolean} options.testing - Whether this is a test build or not.
|
|
* @param options.variables
|
|
* @returns {string} The Infura project ID.
|
|
*/
|
|
function getInfuraProjectId({ buildType, variables, environment, testing }) {
|
|
const EMPTY_PROJECT_ID = '00000000000000000000000000000000';
|
|
if (testing) {
|
|
return EMPTY_PROJECT_ID;
|
|
} else if (environment !== ENVIRONMENT.PRODUCTION) {
|
|
// Skip validation because this is unset on PRs from forks.
|
|
// For forks, return empty project ID if we don't have one.
|
|
if (
|
|
!variables.isDefined('INFURA_PROJECT_ID') &&
|
|
environment === ENVIRONMENT.PULL_REQUEST
|
|
) {
|
|
return EMPTY_PROJECT_ID;
|
|
}
|
|
return variables.get('INFURA_PROJECT_ID');
|
|
}
|
|
/** @type {string|undefined} */
|
|
const infuraKeyReference = variables.get('INFURA_ENV_KEY_REF');
|
|
assert(
|
|
typeof infuraKeyReference === 'string' && infuraKeyReference.length > 0,
|
|
`Build type "${buildType}" has improperly set INFURA_ENV_KEY_REF in builds.yml. Current value: "${infuraKeyReference}"`,
|
|
);
|
|
/** @type {string|undefined} */
|
|
const infuraProjectId = variables.get(infuraKeyReference);
|
|
assert(
|
|
typeof infuraProjectId === 'string' && infuraProjectId.length > 0,
|
|
`Infura Project ID environmental variable "${infuraKeyReference}" is set improperly.`,
|
|
);
|
|
return infuraProjectId;
|
|
}
|
|
|
|
/**
|
|
* Get the appropriate Segment write key.
|
|
*
|
|
* @param {object} options - The Segment write key options.
|
|
* @param {string} options.buildType - The current build type.
|
|
* @param {keyof ENVIRONMENT} options.environment - The current build environment.
|
|
* @param {import('../lib/variables').Variables} options.variables - Object containing all variables that modify the build pipeline
|
|
* @returns {string} The Segment write key.
|
|
*/
|
|
function getSegmentWriteKey({ buildType, variables, environment }) {
|
|
if (environment !== ENVIRONMENT.PRODUCTION) {
|
|
// Skip validation because this is unset on PRs from forks, and isn't necessary for development builds.
|
|
return variables.get('SEGMENT_WRITE_KEY');
|
|
}
|
|
|
|
const segmentKeyReference = variables.get('SEGMENT_WRITE_KEY_REF');
|
|
assert(
|
|
typeof segmentKeyReference === 'string' && segmentKeyReference.length > 0,
|
|
`Build type "${buildType}" has improperly set SEGMENT_WRITE_KEY_REF in builds.yml. Current value: "${segmentKeyReference}"`,
|
|
);
|
|
|
|
const segmentWriteKey = variables.get(segmentKeyReference);
|
|
assert(
|
|
typeof segmentWriteKey === 'string' && segmentWriteKey.length > 0,
|
|
`Segment Write Key environmental variable "${segmentKeyReference}" is set improperly.`,
|
|
);
|
|
return segmentWriteKey;
|
|
}
|
|
|
|
/**
|
|
* Get the URL for the phishing warning page, if it has been set.
|
|
*
|
|
* @param {object} options - The phishing warning page options.
|
|
* @param {boolean} options.testing - Whether this is a test build or not.
|
|
* @param {import('../lib/variables').Variables} options.variables - Object containing all variables that modify the build pipeline
|
|
* @returns {string} The URL for the phishing warning page, or `undefined` if no URL is set.
|
|
*/
|
|
function getPhishingWarningPageUrl({ variables, testing }) {
|
|
let phishingWarningPageUrl = variables.get('PHISHING_WARNING_PAGE_URL');
|
|
|
|
assert(
|
|
phishingWarningPageUrl === null ||
|
|
typeof phishingWarningPageUrl === 'string',
|
|
);
|
|
if (phishingWarningPageUrl === null) {
|
|
phishingWarningPageUrl = testing
|
|
? 'http://localhost:9999/'
|
|
: `https://metamask.github.io/phishing-warning/v${phishingWarningManifest.version}/`;
|
|
}
|
|
|
|
// We add a hash/fragment to the URL dynamically, so we need to ensure it
|
|
// has a valid pathname to append a hash to.
|
|
const normalizedUrl = phishingWarningPageUrl.endsWith('/')
|
|
? phishingWarningPageUrl
|
|
: `${phishingWarningPageUrl}/`;
|
|
|
|
let phishingWarningPageUrlObject;
|
|
try {
|
|
// eslint-disable-next-line no-new
|
|
phishingWarningPageUrlObject = new URL(normalizedUrl);
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Invalid phishing warning page URL: '${normalizedUrl}'`,
|
|
error,
|
|
);
|
|
}
|
|
if (phishingWarningPageUrlObject.hash) {
|
|
// The URL fragment must be set dynamically
|
|
throw new Error(
|
|
`URL fragment not allowed in phishing warning page URL: '${normalizedUrl}'`,
|
|
);
|
|
}
|
|
|
|
return normalizedUrl;
|
|
}
|
|
|
|
const noopWriteStream = through.obj((_file, _fileEncoding, callback) =>
|
|
callback(),
|
|
);
|
|
|
|
module.exports = createScriptTasks;
|
|
|
|
/**
|
|
* Create tasks for building JavaScript bundles and templates. One
|
|
* task is returned for each build target. These build target tasks are
|
|
* each composed of smaller tasks.
|
|
*
|
|
* @param {object} options - Build options.
|
|
* @param {boolean} options.applyLavaMoat - Whether the build should use
|
|
* LavaMoat at runtime or not.
|
|
* @param {string[]} options.browserPlatforms - A list of browser platforms to
|
|
* build bundles for.
|
|
* @param {string} options.buildType - The current build type (e.g. "main",
|
|
* "flask", etc.).
|
|
* @param {string[] | null} options.ignoredFiles - A list of files to exclude
|
|
* from the current build.
|
|
* @param {boolean} options.isLavaMoat - Whether this build script is being run
|
|
* using LavaMoat or not.
|
|
* @param {object} options.livereload - The "gulp-livereload" server instance.
|
|
* @param {boolean} options.policyOnly - Whether to stop the build after
|
|
* generating the LavaMoat policy, skipping any writes to disk other than the
|
|
* LavaMoat policy itself.
|
|
* @param {boolean} options.shouldLintFenceFiles - Whether files with code
|
|
* fences should be linted after fences have been removed.
|
|
* @param {string} options.version - The current version of the extension.
|
|
* @returns {object} A set of tasks, one for each build target.
|
|
*/
|
|
function createScriptTasks({
|
|
applyLavaMoat,
|
|
browserPlatforms,
|
|
buildType,
|
|
ignoredFiles,
|
|
isLavaMoat,
|
|
livereload,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
}) {
|
|
// high level tasks
|
|
return {
|
|
// dev tasks (live reload)
|
|
dev: createTasksForScriptBundles({
|
|
buildTarget: BUILD_TARGETS.DEV,
|
|
taskPrefix: 'scripts:core:dev',
|
|
}),
|
|
// production-like distributable build
|
|
dist: createTasksForScriptBundles({
|
|
buildTarget: BUILD_TARGETS.DIST,
|
|
taskPrefix: 'scripts:core:dist',
|
|
}),
|
|
// production
|
|
prod: createTasksForScriptBundles({
|
|
buildTarget: BUILD_TARGETS.PROD,
|
|
taskPrefix: 'scripts:core:prod',
|
|
}),
|
|
// built for CI tests
|
|
test: createTasksForScriptBundles({
|
|
buildTarget: BUILD_TARGETS.TEST,
|
|
taskPrefix: 'scripts:core:test',
|
|
}),
|
|
// built for CI test debugging
|
|
testDev: createTasksForScriptBundles({
|
|
buildTarget: BUILD_TARGETS.TEST_DEV,
|
|
taskPrefix: 'scripts:core:test-live',
|
|
}),
|
|
};
|
|
|
|
/**
|
|
* Define tasks for building the JavaScript modules used by the extension.
|
|
* This function returns a single task that builds JavaScript modules in
|
|
* parallel for a single type of build (e.g. dev, testing, production).
|
|
*
|
|
* @param {object} options - The build options.
|
|
* @param {BUILD_TARGETS} options.buildTarget - The build target that these
|
|
* JavaScript modules are intended for.
|
|
* @param {string} options.taskPrefix - The prefix to use for the name of
|
|
* each defined task.
|
|
*/
|
|
function createTasksForScriptBundles({ buildTarget, taskPrefix }) {
|
|
const standardEntryPoints = ['background', 'ui', 'content-script'];
|
|
const standardSubtask = createTask(
|
|
`${taskPrefix}:standardEntryPoints`,
|
|
createFactoredBuild({
|
|
applyLavaMoat,
|
|
browserPlatforms,
|
|
buildTarget,
|
|
buildType,
|
|
entryFiles: standardEntryPoints.map((label) => {
|
|
if (label === 'content-script') {
|
|
return './app/vendor/trezor/content-script.js';
|
|
}
|
|
return `./app/scripts/${label}.js`;
|
|
}),
|
|
ignoredFiles,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
}),
|
|
);
|
|
|
|
// inpage must be built before contentscript
|
|
// because inpage bundle result is included inside contentscript
|
|
const contentscriptSubtask = createTask(
|
|
`${taskPrefix}:contentscript`,
|
|
createContentscriptBundle({ buildTarget }),
|
|
);
|
|
|
|
// this can run whenever
|
|
const disableConsoleSubtask = createTask(
|
|
`${taskPrefix}:disable-console`,
|
|
createDisableConsoleBundle({ buildTarget }),
|
|
);
|
|
|
|
// this can run whenever
|
|
const installSentrySubtask = createTask(
|
|
`${taskPrefix}:sentry`,
|
|
createSentryBundle({ buildTarget }),
|
|
);
|
|
|
|
// task for initiating browser livereload
|
|
const initiateLiveReload = async () => {
|
|
if (isDevBuild(buildTarget)) {
|
|
// 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 = [
|
|
standardSubtask,
|
|
contentscriptSubtask,
|
|
disableConsoleSubtask,
|
|
installSentrySubtask,
|
|
].map((subtask) =>
|
|
runInChildProcess(subtask, {
|
|
applyLavaMoat,
|
|
buildType,
|
|
isLavaMoat,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
}),
|
|
);
|
|
// make a parent task that runs each task in a child thread
|
|
return composeParallel(initiateLiveReload, ...allSubtasks);
|
|
}
|
|
|
|
/**
|
|
* Create a bundle for the "disable-console" module.
|
|
*
|
|
* @param {object} options - The build options.
|
|
* @param {BUILD_TARGETS} options.buildTarget - The current build target.
|
|
* @returns {Function} A function that creates the bundle.
|
|
*/
|
|
function createDisableConsoleBundle({ buildTarget }) {
|
|
const label = 'disable-console';
|
|
return createNormalBundle({
|
|
browserPlatforms,
|
|
buildTarget,
|
|
buildType,
|
|
destFilepath: `${label}.js`,
|
|
entryFilepath: `./app/scripts/${label}.js`,
|
|
ignoredFiles,
|
|
label,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
applyLavaMoat,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a bundle for the "sentry-install" module.
|
|
*
|
|
* @param {object} options - The build options.
|
|
* @param {BUILD_TARGETS} options.buildTarget - The current build target.
|
|
* @returns {Function} A function that creates the bundle.
|
|
*/
|
|
function createSentryBundle({ buildTarget }) {
|
|
const label = 'sentry-install';
|
|
return createNormalBundle({
|
|
browserPlatforms,
|
|
buildTarget,
|
|
buildType,
|
|
destFilepath: `${label}.js`,
|
|
entryFilepath: `./app/scripts/${label}.js`,
|
|
ignoredFiles,
|
|
label,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
applyLavaMoat,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create bundles for the "contentscript" and "inpage" modules. The inpage
|
|
* module is created first because it gets embedded in the contentscript
|
|
* module.
|
|
*
|
|
* @param {object} options - The build options.
|
|
* @param {BUILD_TARGETS} options.buildTarget - The current build target.
|
|
* @returns {Function} A function that creates the bundles.
|
|
*/
|
|
function createContentscriptBundle({ buildTarget }) {
|
|
const inpage = 'inpage';
|
|
const contentscript = 'contentscript';
|
|
return composeSeries(
|
|
createNormalBundle({
|
|
buildTarget,
|
|
buildType,
|
|
browserPlatforms,
|
|
destFilepath: `${inpage}.js`,
|
|
entryFilepath: `./app/scripts/${inpage}.js`,
|
|
label: inpage,
|
|
ignoredFiles,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
applyLavaMoat,
|
|
}),
|
|
createNormalBundle({
|
|
buildTarget,
|
|
buildType,
|
|
browserPlatforms,
|
|
destFilepath: `${contentscript}.js`,
|
|
entryFilepath: `./app/scripts/${contentscript}.js`,
|
|
label: contentscript,
|
|
ignoredFiles,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
applyLavaMoat,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create the bundle for the app initialization module used in manifest v3
|
|
* builds.
|
|
*
|
|
* This must be called after the "background" bundles have been created, so
|
|
* that the list of all background bundles can be injected into this bundle.
|
|
*
|
|
* @param {object} options - Build options.
|
|
* @param {boolean} options.applyLavaMoat - Whether the build should use
|
|
* LavaMoat at runtime or not.
|
|
* @param {string[]} options.browserPlatforms - A list of browser platforms to
|
|
* build bundles for.
|
|
* @param {BUILD_TARGETS} options.buildTarget - The current build target.
|
|
* @param {string} options.buildType - The current build type (e.g. "main",
|
|
* "flask", etc.).
|
|
* @param {string[] | null} options.ignoredFiles - A list of files to exclude
|
|
* from the current build.
|
|
* @param {string[]} options.jsBundles - A list of JavaScript bundles to be
|
|
* injected into this bundle.
|
|
* @param {boolean} options.policyOnly - Whether to stop the build after
|
|
* generating the LavaMoat policy, skipping any writes to disk other than the
|
|
* LavaMoat policy itself.
|
|
* @param {boolean} options.shouldLintFenceFiles - Whether files with code
|
|
* fences should be linted after fences have been removed.
|
|
* @param {string} options.version - The current version of the extension.
|
|
* @returns {Function} A function that creates the set of bundles.
|
|
*/
|
|
async function createManifestV3AppInitializationBundle({
|
|
applyLavaMoat,
|
|
browserPlatforms,
|
|
buildTarget,
|
|
buildType,
|
|
ignoredFiles,
|
|
jsBundles,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
}) {
|
|
const label = 'app-init';
|
|
// TODO: remove this filter for firefox once MV3 is supported in it
|
|
const mv3BrowserPlatforms = browserPlatforms.filter(
|
|
(platform) => platform !== 'firefox',
|
|
);
|
|
|
|
for (const filename of jsBundles) {
|
|
if (filename.includes(',')) {
|
|
throw new Error(
|
|
`Invalid filename "${filename}", not allowed to contain comma.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const extraEnvironmentVariables = {
|
|
APPLY_LAVAMOAT: applyLavaMoat,
|
|
FILE_NAMES: jsBundles.join(','),
|
|
};
|
|
|
|
await createNormalBundle({
|
|
browserPlatforms: mv3BrowserPlatforms,
|
|
buildTarget,
|
|
buildType,
|
|
destFilepath: 'app-init.js',
|
|
entryFilepath: './app/scripts/app-init.js',
|
|
extraEnvironmentVariables,
|
|
ignoredFiles,
|
|
label,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
applyLavaMoat,
|
|
})();
|
|
|
|
// Code below is used to set statsMode to true when testing in MV3
|
|
// This is used to capture module initialisation stats using lavamoat.
|
|
if (isTestBuild(buildTarget)) {
|
|
const content = readFileSync('./dist/chrome/runtime-lavamoat.js', 'utf8');
|
|
const fileOutput = content.replace('statsMode = false', 'statsMode = true');
|
|
writeFileSync('./dist/chrome/runtime-lavamoat.js', fileOutput);
|
|
}
|
|
|
|
console.log(`Bundle end: service worker app-init.js`);
|
|
}
|
|
|
|
/**
|
|
* Return a function that creates a set of factored bundles.
|
|
*
|
|
* For each entry point, a series of one or more bundles is created. These are
|
|
* split up roughly by size, to ensure no single bundle exceeds the maximum
|
|
* JavaScript file size imposed by Firefox.
|
|
*
|
|
* Modules that are common between all entry points are bundled separately, as
|
|
* a set of one or more "common" bundles.
|
|
*
|
|
* @param {object} options - Build options.
|
|
* @param {boolean} options.applyLavaMoat - Whether the build should use
|
|
* LavaMoat at runtime or not.
|
|
* @param {string[]} options.browserPlatforms - A list of browser platforms to
|
|
* build bundles for.
|
|
* @param {BUILD_TARGETS} options.buildTarget - The current build target.
|
|
* @param {string} options.buildType - The current build type (e.g. "main",
|
|
* "flask", etc.).
|
|
* @param {string[]} options.entryFiles - A list of entry point file paths,
|
|
* relative to the repository root directory.
|
|
* @param {string[] | null} options.ignoredFiles - A list of files to exclude
|
|
* from the current build.
|
|
* @param {boolean} options.policyOnly - Whether to stop the build after
|
|
* generating the LavaMoat policy, skipping any writes to disk other than the
|
|
* LavaMoat policy itself.
|
|
* @param {boolean} options.shouldLintFenceFiles - Whether files with code
|
|
* fences should be linted after fences have been removed.
|
|
* @param {string} options.version - The current version of the extension.
|
|
* @returns {Function} A function that creates the set of bundles.
|
|
*/
|
|
function createFactoredBuild({
|
|
applyLavaMoat,
|
|
browserPlatforms,
|
|
buildTarget,
|
|
buildType,
|
|
entryFiles,
|
|
ignoredFiles,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
}) {
|
|
return async function () {
|
|
// create bundler setup and apply defaults
|
|
const buildConfiguration = createBuildConfiguration();
|
|
buildConfiguration.label = 'primary';
|
|
const { bundlerOpts, events } = buildConfiguration;
|
|
|
|
// devMode options
|
|
const reloadOnChange = isDevBuild(buildTarget);
|
|
const minify = !isDevBuild(buildTarget);
|
|
|
|
const environment = getEnvironment({ buildTarget });
|
|
const config = await getConfig(buildType, environment);
|
|
const { variables, activeBuild } = config;
|
|
await setEnvironmentVariables({
|
|
buildTarget,
|
|
buildType,
|
|
environment,
|
|
variables,
|
|
activeBuild,
|
|
version,
|
|
});
|
|
const features = {
|
|
active: new Set(activeBuild.features ?? []),
|
|
all: new Set(Object.keys(config.buildsYml.features)),
|
|
};
|
|
setupBundlerDefaults(buildConfiguration, {
|
|
buildTarget,
|
|
variables,
|
|
envVars: buildSafeVariableObject(variables),
|
|
ignoredFiles,
|
|
policyOnly,
|
|
minify,
|
|
features,
|
|
reloadOnChange,
|
|
shouldLintFenceFiles,
|
|
});
|
|
|
|
// set bundle entries
|
|
bundlerOpts.entries = [...entryFiles];
|
|
|
|
// setup lavamoat
|
|
// lavamoat will add lavapack but it will be removed by bify-module-groups
|
|
// we will re-add it later by installing a lavapack runtime
|
|
const lavamoatOpts = {
|
|
policy: path.resolve(
|
|
__dirname,
|
|
`../../lavamoat/browserify/${buildType}/policy.json`,
|
|
),
|
|
policyName: buildType,
|
|
policyOverride: path.resolve(
|
|
__dirname,
|
|
`../../lavamoat/browserify/policy-override.json`,
|
|
),
|
|
writeAutoPolicy: process.env.WRITE_AUTO_POLICY,
|
|
};
|
|
Object.assign(bundlerOpts, lavamoatBrowserify.args);
|
|
bundlerOpts.plugin.push([lavamoatBrowserify, lavamoatOpts]);
|
|
|
|
// setup bundle factoring with bify-module-groups plugin
|
|
// note: this will remove lavapack, but its ok bc we manually readd it later
|
|
Object.assign(bundlerOpts, bifyModuleGroups.plugin.args);
|
|
bundlerOpts.plugin = [...bundlerOpts.plugin, [bifyModuleGroups.plugin]];
|
|
|
|
// instrument pipeline
|
|
let sizeGroupMap;
|
|
events.on('configurePipeline', ({ pipeline }) => {
|
|
// to be populated by the group-by-size transform
|
|
sizeGroupMap = new Map();
|
|
pipeline.get('groups').unshift(
|
|
// factor modules
|
|
bifyModuleGroups.groupByFactor({
|
|
entryFileToLabel(filepath) {
|
|
return path.parse(filepath).name;
|
|
},
|
|
}),
|
|
// cap files at 2 mb
|
|
bifyModuleGroups.groupBySize({
|
|
sizeLimit: 2e6,
|
|
groupingMap: sizeGroupMap,
|
|
}),
|
|
);
|
|
// converts each module group into a single vinyl file containing its bundle
|
|
const moduleGroupPackerStream = streamFlatMap((moduleGroup) => {
|
|
const filename = `${moduleGroup.label}.js`;
|
|
const childStream = wrapInStream(
|
|
moduleGroup.stream,
|
|
// we manually readd lavapack here bc bify-module-groups removes it
|
|
lavapack({ raw: true, hasExports: true, includePrelude: false }),
|
|
source(filename),
|
|
);
|
|
return childStream;
|
|
});
|
|
pipeline.get('vinyl').unshift(moduleGroupPackerStream, buffer());
|
|
// add lavamoat policy loader file to packer output
|
|
moduleGroupPackerStream.push(
|
|
new Vinyl({
|
|
path: 'policy-load.js',
|
|
contents: lavapack.makePolicyLoaderStream(lavamoatOpts),
|
|
}),
|
|
);
|
|
// setup bundle destination
|
|
browserPlatforms.forEach((platform) => {
|
|
const dest = `./dist/${platform}/`;
|
|
const destination = policyOnly ? noopWriteStream : gulp.dest(dest);
|
|
pipeline.get('dest').push(destination);
|
|
});
|
|
});
|
|
|
|
// wait for bundle completion for postprocessing
|
|
events.on('bundleDone', async () => {
|
|
// Skip HTML generation if nothing is to be written to disk
|
|
if (policyOnly) {
|
|
return;
|
|
}
|
|
const commonSet = sizeGroupMap.get('common');
|
|
// create entry points for each file
|
|
for (const [groupLabel, groupSet] of sizeGroupMap.entries()) {
|
|
// skip "common" group, they are added to all other groups
|
|
if (groupSet === commonSet) {
|
|
continue;
|
|
}
|
|
|
|
switch (groupLabel) {
|
|
case 'ui': {
|
|
renderHtmlFile({
|
|
htmlName: 'popup',
|
|
groupSet,
|
|
commonSet,
|
|
browserPlatforms,
|
|
applyLavaMoat,
|
|
});
|
|
renderHtmlFile({
|
|
htmlName: 'notification',
|
|
groupSet,
|
|
commonSet,
|
|
browserPlatforms,
|
|
applyLavaMoat,
|
|
});
|
|
renderHtmlFile({
|
|
htmlName: 'home',
|
|
groupSet,
|
|
commonSet,
|
|
browserPlatforms,
|
|
applyLavaMoat,
|
|
isMMI: buildType === 'mmi',
|
|
});
|
|
break;
|
|
}
|
|
case 'background': {
|
|
renderHtmlFile({
|
|
htmlName: 'background',
|
|
groupSet,
|
|
commonSet,
|
|
browserPlatforms,
|
|
applyLavaMoat,
|
|
});
|
|
if (process.env.ENABLE_MV3) {
|
|
const jsBundles = [
|
|
...commonSet.values(),
|
|
...groupSet.values(),
|
|
].map((label) => `./${label}.js`);
|
|
await createManifestV3AppInitializationBundle({
|
|
applyLavaMoat,
|
|
browserPlatforms,
|
|
buildTarget,
|
|
buildType,
|
|
ignoredFiles,
|
|
jsBundles,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case 'content-script': {
|
|
renderHtmlFile({
|
|
htmlName: 'trezor-usb-permissions',
|
|
groupSet,
|
|
commonSet,
|
|
browserPlatforms,
|
|
applyLavaMoat: false,
|
|
});
|
|
break;
|
|
}
|
|
default: {
|
|
throw new Error(
|
|
`build/scripts - unknown groupLabel "${groupLabel}"`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
await createBundle(buildConfiguration, { reloadOnChange });
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Return a function that creates a single JavaScript bundle.
|
|
*
|
|
* @param {object} options - Build options.
|
|
* @param {string[]} options.browserPlatforms - A list of browser platforms to
|
|
* build the bundle for.
|
|
* @param {BUILD_TARGETS} options.buildTarget - The current build target.
|
|
* @param {string} options.buildType - The current build type (e.g. "main",
|
|
* "flask", etc.).
|
|
* @param {string} options.destFilepath - The file path the bundle should be
|
|
* written to.
|
|
* @param {string[]} options.entryFilepath - The entry point file path,
|
|
* relative to the repository root directory.
|
|
* @param {Record<string, unknown>} options.extraEnvironmentVariables - Extra
|
|
* environment variables to inject just into this bundle.
|
|
* @param {string[] | null} options.ignoredFiles - A list of files to exclude
|
|
* from the current build.
|
|
* @param {string} options.label - A label used to describe this bundle in any
|
|
* diagnostic messages.
|
|
* @param {boolean} options.policyOnly - Whether to stop the build after
|
|
* generating the LavaMoat policy, skipping any writes to disk other than the
|
|
* LavaMoat policy itself.
|
|
* @param {boolean} options.shouldLintFenceFiles - Whether files with code
|
|
* fences should be linted after fences have been removed.
|
|
* @param {string} options.version - The current version of the extension.
|
|
* @param {boolean} options.applyLavaMoat - Whether to apply LavaMoat or not
|
|
* @returns {Function} A function that creates the bundle.
|
|
*/
|
|
function createNormalBundle({
|
|
browserPlatforms,
|
|
buildTarget,
|
|
buildType,
|
|
destFilepath,
|
|
entryFilepath,
|
|
extraEnvironmentVariables,
|
|
ignoredFiles,
|
|
label,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
applyLavaMoat,
|
|
}) {
|
|
return async function () {
|
|
// create bundler setup and apply defaults
|
|
const buildConfiguration = createBuildConfiguration();
|
|
buildConfiguration.label = label;
|
|
const { bundlerOpts, events } = buildConfiguration;
|
|
|
|
// devMode options
|
|
const devMode = isDevBuild(buildTarget);
|
|
const reloadOnChange = Boolean(devMode);
|
|
const minify = Boolean(devMode) === false;
|
|
|
|
const environment = getEnvironment({ buildTarget });
|
|
const config = await getConfig(buildType, environment);
|
|
const { activeBuild, variables } = config;
|
|
await setEnvironmentVariables({
|
|
buildTarget,
|
|
buildType,
|
|
variables,
|
|
environment,
|
|
activeBuild,
|
|
version,
|
|
});
|
|
Object.entries(extraEnvironmentVariables ?? {}).forEach(([key, value]) =>
|
|
variables.set(key, value),
|
|
);
|
|
|
|
const features = {
|
|
active: new Set(activeBuild.features ?? []),
|
|
all: new Set(Object.keys(config.buildsYml.features)),
|
|
};
|
|
setupBundlerDefaults(buildConfiguration, {
|
|
envVars: buildSafeVariableObject(variables),
|
|
ignoredFiles,
|
|
policyOnly,
|
|
minify,
|
|
features,
|
|
reloadOnChange,
|
|
shouldLintFenceFiles,
|
|
applyLavaMoat,
|
|
});
|
|
|
|
// set bundle entries
|
|
bundlerOpts.entries = [entryFilepath];
|
|
|
|
// instrument pipeline
|
|
events.on('configurePipeline', ({ pipeline }) => {
|
|
// convert bundle stream to gulp vinyl stream
|
|
// and ensure file contents are buffered
|
|
pipeline.get('vinyl').push(source(destFilepath));
|
|
pipeline.get('vinyl').push(buffer());
|
|
// setup bundle destination
|
|
browserPlatforms.forEach((platform) => {
|
|
const dest = `./dist/${platform}/`;
|
|
const destination = policyOnly ? noopWriteStream : gulp.dest(dest);
|
|
pipeline.get('dest').push(destination);
|
|
});
|
|
});
|
|
|
|
await createBundle(buildConfiguration, { reloadOnChange });
|
|
};
|
|
}
|
|
|
|
function createBuildConfiguration() {
|
|
const label = '(unnamed bundle)';
|
|
const events = new EventEmitter();
|
|
const bundlerOpts = {
|
|
entries: [],
|
|
transform: [],
|
|
plugin: [],
|
|
require: [],
|
|
// non-standard bify options
|
|
manualExternal: [],
|
|
manualIgnore: [],
|
|
};
|
|
return { bundlerOpts, events, label };
|
|
}
|
|
|
|
function setupBundlerDefaults(
|
|
buildConfiguration,
|
|
{
|
|
buildTarget,
|
|
envVars,
|
|
ignoredFiles,
|
|
policyOnly,
|
|
minify,
|
|
features,
|
|
reloadOnChange,
|
|
shouldLintFenceFiles,
|
|
applyLavaMoat,
|
|
},
|
|
) {
|
|
const { bundlerOpts } = buildConfiguration;
|
|
const extensions = ['.js', '.ts', '.tsx'];
|
|
|
|
const isSnapsFlask =
|
|
features.active.has('snaps') && features.active.has('build-flask');
|
|
|
|
Object.assign(bundlerOpts, {
|
|
// Source transforms
|
|
transform: [
|
|
// // Remove code that should be excluded from builds of the current type
|
|
createRemoveFencedCodeTransform(features, shouldLintFenceFiles),
|
|
// Transpile top-level code
|
|
[
|
|
babelify,
|
|
// Run TypeScript files through Babel
|
|
{
|
|
extensions,
|
|
plugins: isSnapsFlask
|
|
? [
|
|
[
|
|
moduleResolver,
|
|
{
|
|
alias: {
|
|
'@metamask/snaps-controllers':
|
|
'@metamask/snaps-controllers-flask',
|
|
'@metamask/snaps-ui': '@metamask/snaps-ui-flask',
|
|
'@metamask/snaps-utils': '@metamask/snaps-utils-flask',
|
|
'@metamask/rpc-methods': '@metamask/rpc-methods-flask',
|
|
},
|
|
},
|
|
],
|
|
]
|
|
: [],
|
|
},
|
|
],
|
|
// Inline `fs.readFileSync` files
|
|
brfs,
|
|
],
|
|
// Look for TypeScript files when walking the dependency tree
|
|
extensions,
|
|
// Use entryFilepath for moduleIds, easier to determine origin file
|
|
fullPaths: isDevBuild(buildTarget) || isTestBuild(buildTarget),
|
|
// For sourcemaps
|
|
debug: true,
|
|
});
|
|
|
|
// Ensure react-devtools is only included in dev builds
|
|
if (buildTarget !== BUILD_TARGETS.DEV) {
|
|
bundlerOpts.manualIgnore.push('react-devtools');
|
|
bundlerOpts.manualIgnore.push('remote-redux-devtools');
|
|
}
|
|
|
|
// This dependency uses WASM which we cannot execute in accordance with our CSP
|
|
bundlerOpts.manualIgnore.push('@chainsafe/as-sha256');
|
|
|
|
// Inject environment variables via node-style `process.env`
|
|
if (envVars) {
|
|
bundlerOpts.transform.push([envify(envVars), { global: true }]);
|
|
}
|
|
|
|
// Ensure that any files that should be ignored are excluded from the build
|
|
if (ignoredFiles) {
|
|
bundlerOpts.manualExclude = ignoredFiles;
|
|
}
|
|
|
|
// Setup reload on change
|
|
if (reloadOnChange) {
|
|
setupReloadOnChange(buildConfiguration);
|
|
}
|
|
|
|
if (!policyOnly) {
|
|
if (minify) {
|
|
setupMinification(buildConfiguration);
|
|
}
|
|
|
|
// Setup source maps
|
|
setupSourcemaps(buildConfiguration, { buildTarget });
|
|
// Setup wrapping of code against scuttling (before sourcemaps generation)
|
|
setupScuttlingWrapping(buildConfiguration, applyLavaMoat, envVars);
|
|
}
|
|
}
|
|
|
|
function setupReloadOnChange({ bundlerOpts, events }) {
|
|
// Add plugin to options
|
|
Object.assign(bundlerOpts, {
|
|
plugin: [...bundlerOpts.plugin, watchify],
|
|
// Required by watchify
|
|
cache: {},
|
|
packageCache: {},
|
|
});
|
|
// Instrument pipeline
|
|
events.on('configurePipeline', ({ bundleStream }) => {
|
|
// Handle build error to avoid breaking build process
|
|
// (eg on syntax error)
|
|
bundleStream.on('error', (err) => {
|
|
gracefulError(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
function setupMinification(buildConfiguration) {
|
|
const minifyOpts = {
|
|
mangle: {
|
|
reserved: ['MetamaskInpageProvider'],
|
|
},
|
|
};
|
|
const { events } = buildConfiguration;
|
|
events.on('configurePipeline', ({ pipeline }) => {
|
|
pipeline.get('minify').push(
|
|
// this is the "gulp-terser-js" wrapper around the latest version of terser
|
|
through.obj(
|
|
callbackify(async (file, _enc) => {
|
|
const input = {
|
|
[file.sourceMap.file]: file.contents.toString(),
|
|
};
|
|
const opts = {
|
|
sourceMap: {
|
|
filename: file.sourceMap.file,
|
|
content: file.sourceMap,
|
|
},
|
|
...minifyOpts,
|
|
};
|
|
const res = await terser.minify(input, opts);
|
|
file.contents = Buffer.from(res.code);
|
|
applySourceMap(file, res.map);
|
|
return file;
|
|
}),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
function setupScuttlingWrapping(buildConfiguration, applyLavaMoat, envVars) {
|
|
const scuttlingConfig =
|
|
envVars.ENABLE_MV3 === 'true'
|
|
? mv3ScuttlingConfig
|
|
: standardScuttlingConfig;
|
|
const { events } = buildConfiguration;
|
|
events.on('configurePipeline', ({ pipeline }) => {
|
|
pipeline.get('scuttle').push(
|
|
through.obj(
|
|
callbackify(async (file, _enc) => {
|
|
const configForFile = scuttlingConfig[file.relative];
|
|
if (applyLavaMoat && configForFile) {
|
|
const wrapped = wrapAgainstScuttling(
|
|
file.contents.toString(),
|
|
configForFile,
|
|
);
|
|
file.contents = Buffer.from(wrapped, 'utf8');
|
|
}
|
|
return file;
|
|
}),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
function setupSourcemaps(buildConfiguration, { buildTarget }) {
|
|
const { events } = buildConfiguration;
|
|
events.on('configurePipeline', ({ pipeline }) => {
|
|
pipeline.get('sourcemaps:init').push(sourcemaps.init({ loadMaps: true }));
|
|
pipeline
|
|
.get('sourcemaps:write')
|
|
// Use inline source maps for development due to Chrome DevTools bug
|
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=931675
|
|
.push(
|
|
isDevBuild(buildTarget)
|
|
? sourcemaps.write()
|
|
: sourcemaps.write('../sourcemaps', { addComment: false }),
|
|
);
|
|
});
|
|
}
|
|
|
|
async function createBundle(buildConfiguration, { reloadOnChange }) {
|
|
const { label, bundlerOpts, events } = buildConfiguration;
|
|
const bundler = browserify(bundlerOpts);
|
|
|
|
// manually apply non-standard options
|
|
bundler.external(bundlerOpts.manualExternal);
|
|
bundler.ignore(bundlerOpts.manualIgnore);
|
|
if (Array.isArray(bundlerOpts.manualExclude)) {
|
|
bundler.exclude(bundlerOpts.manualExclude);
|
|
}
|
|
|
|
// output build logs to terminal
|
|
bundler.on('log', log);
|
|
|
|
// forward update event (used by watchify)
|
|
bundler.on('update', () => performBundle());
|
|
|
|
console.log(`Bundle start: "${label}"`);
|
|
await performBundle();
|
|
console.log(`Bundle end: "${label}"`);
|
|
|
|
async function performBundle() {
|
|
// this pipeline is created for every bundle
|
|
// the labels are all the steps you can hook into
|
|
const pipeline = labeledStreamSplicer([
|
|
'groups',
|
|
[],
|
|
'vinyl',
|
|
[],
|
|
'scuttle',
|
|
[],
|
|
'sourcemaps:init',
|
|
[],
|
|
'minify',
|
|
[],
|
|
'sourcemaps:write',
|
|
[],
|
|
'dest',
|
|
[],
|
|
]);
|
|
const bundleStream = bundler.bundle();
|
|
if (!reloadOnChange) {
|
|
bundleStream.on('error', (error) => {
|
|
console.error('Bundling failed! See details below.');
|
|
logError(error);
|
|
process.exit(1);
|
|
});
|
|
pipeline.on('error', (error) => {
|
|
console.error('Pipeline failed! See details below.');
|
|
logError(error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
// trigger build pipeline instrumentations
|
|
events.emit('configurePipeline', { pipeline, bundleStream });
|
|
// start bundle, send into pipeline
|
|
bundleStream.pipe(pipeline);
|
|
// nothing will consume pipeline, so let it flow
|
|
pipeline.resume();
|
|
|
|
await endOfStream(pipeline);
|
|
|
|
// call the completion event to handle any post-processing
|
|
events.emit('bundleDone');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets environment variables to inject in the current build.
|
|
*
|
|
* @param {object} options - Build options.
|
|
* @param {BUILD_TARGETS} options.buildTarget - The current build target.
|
|
* @param {string} options.buildType - The current build type (e.g. "main",
|
|
* "flask", etc.).
|
|
* @param {string} options.version - The current version of the extension.
|
|
* @param options.activeBuild
|
|
* @param options.variables
|
|
* @param options.environment
|
|
*/
|
|
async function setEnvironmentVariables({
|
|
buildTarget,
|
|
buildType,
|
|
activeBuild,
|
|
environment,
|
|
variables,
|
|
version,
|
|
}) {
|
|
const devMode = isDevBuild(buildTarget);
|
|
const testing = isTestBuild(buildTarget);
|
|
const iconNames = await generateIconNames();
|
|
|
|
variables.set({
|
|
ICON_NAMES: iconNames,
|
|
IN_TEST: testing,
|
|
INFURA_PROJECT_ID: getInfuraProjectId({
|
|
buildType,
|
|
activeBuild,
|
|
variables,
|
|
environment,
|
|
testing,
|
|
}),
|
|
METAMASK_DEBUG: devMode || variables.getMaybe('METAMASK_DEBUG') === true,
|
|
METAMASK_ENVIRONMENT: environment,
|
|
METAMASK_VERSION: version,
|
|
METAMASK_BUILD_TYPE: buildType,
|
|
NODE_ENV: devMode ? ENVIRONMENT.DEVELOPMENT : ENVIRONMENT.PRODUCTION,
|
|
PHISHING_WARNING_PAGE_URL: getPhishingWarningPageUrl({
|
|
variables,
|
|
testing,
|
|
}),
|
|
SEGMENT_WRITE_KEY: getSegmentWriteKey({
|
|
buildType,
|
|
activeBuild,
|
|
variables,
|
|
environment,
|
|
}),
|
|
});
|
|
}
|
|
|
|
function renderHtmlFile({
|
|
htmlName,
|
|
groupSet,
|
|
commonSet,
|
|
browserPlatforms,
|
|
applyLavaMoat,
|
|
isMMI,
|
|
}) {
|
|
if (applyLavaMoat === undefined) {
|
|
throw new Error(
|
|
'build/scripts/renderHtmlFile - must specify "applyLavaMoat" option',
|
|
);
|
|
}
|
|
const htmlFilePath = `./app/${htmlName}.html`;
|
|
const htmlTemplate = readFileSync(htmlFilePath, 'utf8');
|
|
const jsBundles = [...commonSet.values(), ...groupSet.values()].map(
|
|
(label) => `./${label}.js`,
|
|
);
|
|
const htmlOutput = Sqrl.render(htmlTemplate, {
|
|
jsBundles,
|
|
applyLavaMoat,
|
|
isMMI,
|
|
});
|
|
browserPlatforms.forEach((platform) => {
|
|
const dest = `./dist/${platform}/${htmlName}.html`;
|
|
// we dont have a way of creating async events atm
|
|
writeFileSync(dest, htmlOutput);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Builds a javascript object that throws an error when trying to access a property that wasn't defined properly
|
|
*
|
|
* @param {Variables} variables
|
|
* @returns {{[key: string]: unknown }} Variable definitions
|
|
*/
|
|
function buildSafeVariableObject(variables) {
|
|
return new Proxy(
|
|
{},
|
|
{
|
|
has(_, key) {
|
|
return key !== '_'; // loose-envify uses "_" for settings
|
|
},
|
|
get(_, key) {
|
|
if (key === '_') {
|
|
return undefined; // loose-envify uses "_" for settings
|
|
}
|
|
return variables.get(key);
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
function beep() {
|
|
process.stdout.write('\x07');
|
|
}
|
|
|
|
function gracefulError(err) {
|
|
console.warn(err);
|
|
beep();
|
|
}
|