mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
e3420a4262
There is a SES bug that results in errors being printed to the console as `{}`[1]. The known workaround is to print the error stack rather than printing the error directly. This affects our build script when it is run with LavaMoat. We used this workaround in one place in the build script already, but not in the handler for task errors. We now use it in both places. The workaround has been moved to a function that we can use throughout the build script. [1]: https://github.com/endojs/endo/issues/944
1174 lines
37 KiB
JavaScript
1174 lines
37 KiB
JavaScript
const { callbackify } = require('util');
|
|
const path = require('path');
|
|
const { writeFileSync, readFileSync } = require('fs');
|
|
const EventEmitter = require('events');
|
|
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 ini = require('ini');
|
|
|
|
const bifyModuleGroups = require('bify-module-groups');
|
|
|
|
const configPath = path.resolve(__dirname, '..', '..', '.metamaskrc');
|
|
let configContents = '';
|
|
try {
|
|
configContents = readFileSync(configPath, {
|
|
encoding: 'utf8',
|
|
});
|
|
} catch (error) {
|
|
if (error.code !== 'ENOENT') {
|
|
throw error;
|
|
}
|
|
}
|
|
const metamaskrc = {
|
|
INFURA_PROJECT_ID: process.env.INFURA_PROJECT_ID,
|
|
INFURA_BETA_PROJECT_ID: process.env.INFURA_BETA_PROJECT_ID,
|
|
INFURA_FLASK_PROJECT_ID: process.env.INFURA_FLASK_PROJECT_ID,
|
|
INFURA_PROD_PROJECT_ID: process.env.INFURA_PROD_PROJECT_ID,
|
|
ONBOARDING_V2: process.env.ONBOARDING_V2,
|
|
COLLECTIBLES_V1: process.env.COLLECTIBLES_V1,
|
|
PHISHING_WARNING_PAGE_URL: process.env.PHISHING_WARNING_PAGE_URL,
|
|
TOKEN_DETECTION_V2: process.env.TOKEN_DETECTION_V2,
|
|
SEGMENT_HOST: process.env.SEGMENT_HOST,
|
|
SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY,
|
|
SEGMENT_BETA_WRITE_KEY: process.env.SEGMENT_BETA_WRITE_KEY,
|
|
SEGMENT_FLASK_WRITE_KEY: process.env.SEGMENT_FLASK_WRITE_KEY,
|
|
SEGMENT_PROD_WRITE_KEY: process.env.SEGMENT_PROD_WRITE_KEY,
|
|
SENTRY_DSN_DEV:
|
|
process.env.SENTRY_DSN_DEV ||
|
|
'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496',
|
|
SIWE_V1: process.env.SIWE_V1,
|
|
...ini.parse(configContents),
|
|
};
|
|
|
|
const { streamFlatMap } = require('../stream-flat-map');
|
|
const { BuildType } = require('../lib/build-type');
|
|
const { BUILD_TARGETS } = require('./constants');
|
|
const { logError } = require('./utils');
|
|
|
|
const {
|
|
createTask,
|
|
composeParallel,
|
|
composeSeries,
|
|
runInChildProcess,
|
|
} = require('./task');
|
|
const {
|
|
createRemoveFencedCodeTransform,
|
|
} = require('./transforms/remove-fenced-code');
|
|
|
|
/**
|
|
* The build environment. This describes the environment this build was produced in.
|
|
*/
|
|
const ENVIRONMENT = {
|
|
DEVELOPMENT: 'development',
|
|
PRODUCTION: 'production',
|
|
OTHER: 'other',
|
|
PULL_REQUEST: 'pull-request',
|
|
RELEASE_CANDIDATE: 'release-candidate',
|
|
STAGING: 'staging',
|
|
TESTING: 'testing',
|
|
};
|
|
|
|
/**
|
|
* Returns whether the current build is a development build or not.
|
|
*
|
|
* @param {BUILD_TARGETS} buildTarget - The current build target.
|
|
* @returns Whether the current build is a development build.
|
|
*/
|
|
function isDevBuild(buildTarget) {
|
|
return (
|
|
buildTarget === BUILD_TARGETS.DEVELOPMENT ||
|
|
buildTarget === BUILD_TARGETS.E2E_TEST_DEV
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the current build is an e2e test build or not.
|
|
*
|
|
* @param {BUILD_TARGETS} buildTarget - The current build target.
|
|
* @returns Whether the current build is an e2e test build.
|
|
*/
|
|
function isTestBuild(buildTarget) {
|
|
return (
|
|
buildTarget === BUILD_TARGETS.E2E_TEST ||
|
|
buildTarget === BUILD_TARGETS.E2E_TEST_DEV
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get a value from the configuration, and confirm that it is set.
|
|
*
|
|
* @param {string} key - The configuration key to retrieve.
|
|
* @returns {string} The config entry requested.
|
|
* @throws {Error} Throws if the requested key is missing.
|
|
*/
|
|
function getConfigValue(key) {
|
|
const value = metamaskrc[key];
|
|
if (!value) {
|
|
throw new Error(`Missing config entry for '${key}'`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Get the appropriate Infura project ID.
|
|
*
|
|
* @param {object} options - The Infura project ID options.
|
|
* @param {BuildType} 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.
|
|
* @returns {string} The Infura project ID.
|
|
*/
|
|
function getInfuraProjectId({ buildType, environment, testing }) {
|
|
if (testing) {
|
|
return '00000000000000000000000000000000';
|
|
} else if (environment !== ENVIRONMENT.PRODUCTION) {
|
|
// Skip validation because this is unset on PRs from forks.
|
|
return metamaskrc.INFURA_PROJECT_ID;
|
|
} else if (buildType === BuildType.main) {
|
|
return getConfigValue('INFURA_PROD_PROJECT_ID');
|
|
} else if (buildType === BuildType.beta) {
|
|
return getConfigValue('INFURA_BETA_PROJECT_ID');
|
|
} else if (buildType === BuildType.flask) {
|
|
return getConfigValue('INFURA_FLASK_PROJECT_ID');
|
|
}
|
|
throw new Error(`Invalid build type: '${buildType}'`);
|
|
}
|
|
|
|
/**
|
|
* Get the appropriate Segment write key.
|
|
*
|
|
* @param {object} options - The Segment write key options.
|
|
* @param {BuildType} options.buildType - The current build type.
|
|
* @param {keyof ENVIRONMENT} options.environment - The current build environment.
|
|
* @returns {string} The Segment write key.
|
|
*/
|
|
function getSegmentWriteKey({ buildType, environment }) {
|
|
if (environment !== ENVIRONMENT.PRODUCTION) {
|
|
// Skip validation because this is unset on PRs from forks, and isn't necessary for development builds.
|
|
return metamaskrc.SEGMENT_WRITE_KEY;
|
|
} else if (buildType === BuildType.main) {
|
|
return getConfigValue('SEGMENT_PROD_WRITE_KEY');
|
|
} else if (buildType === BuildType.beta) {
|
|
return getConfigValue('SEGMENT_BETA_WRITE_KEY');
|
|
} else if (buildType === BuildType.flask) {
|
|
return getConfigValue('SEGMENT_FLASK_WRITE_KEY');
|
|
}
|
|
throw new Error(`Invalid build type: '${buildType}'`);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @returns {string} The URL for the phishing warning page, or `undefined` if no URL is set.
|
|
*/
|
|
function getPhishingWarningPageUrl({ testing }) {
|
|
let phishingWarningPageUrl = metamaskrc.PHISHING_WARNING_PAGE_URL;
|
|
|
|
if (!phishingWarningPageUrl) {
|
|
phishingWarningPageUrl = testing
|
|
? 'http://localhost:9999/'
|
|
: 'https://metamask.github.io/phishing-warning/v1.1.0/';
|
|
}
|
|
|
|
// 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 {BuildType} 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,
|
|
}) {
|
|
// internal tasks
|
|
const core = {
|
|
// dev tasks (live reload)
|
|
dev: createTasksForScriptBundles({
|
|
buildTarget: BUILD_TARGETS.DEVELOPMENT,
|
|
taskPrefix: 'scripts:core:dev',
|
|
}),
|
|
// production
|
|
prod: createTasksForScriptBundles({
|
|
buildTarget: BUILD_TARGETS.PRODUCTION,
|
|
taskPrefix: 'scripts:core:prod',
|
|
}),
|
|
// built for CI tests
|
|
test: createTasksForScriptBundles({
|
|
buildTarget: BUILD_TARGETS.E2E_TEST,
|
|
taskPrefix: 'scripts:core:test',
|
|
}),
|
|
// built for CI test debugging
|
|
testDev: createTasksForScriptBundles({
|
|
buildTarget: BUILD_TARGETS.E2E_TEST_DEV,
|
|
taskPrefix: 'scripts:core:test-live',
|
|
}),
|
|
};
|
|
|
|
// high level tasks
|
|
|
|
const { dev, test, testDev, prod } = core;
|
|
return { dev, test, testDev, prod };
|
|
|
|
/**
|
|
* 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,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
}),
|
|
createNormalBundle({
|
|
buildTarget,
|
|
buildType,
|
|
browserPlatforms,
|
|
destFilepath: `${contentscript}.js`,
|
|
entryFilepath: `./app/scripts/${contentscript}.js`,
|
|
label: contentscript,
|
|
ignoredFiles,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {BuildType} 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,
|
|
})();
|
|
|
|
// 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 {BuildType} 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 envVars = getEnvironmentVariables({
|
|
buildTarget,
|
|
buildType,
|
|
version,
|
|
});
|
|
setupBundlerDefaults(buildConfiguration, {
|
|
buildTarget,
|
|
buildType,
|
|
envVars,
|
|
ignoredFiles,
|
|
policyOnly,
|
|
minify,
|
|
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,
|
|
});
|
|
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 {BuildType} 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.
|
|
* @returns {Function} A function that creates the bundle.
|
|
*/
|
|
function createNormalBundle({
|
|
browserPlatforms,
|
|
buildTarget,
|
|
buildType,
|
|
destFilepath,
|
|
entryFilepath,
|
|
extraEnvironmentVariables,
|
|
ignoredFiles,
|
|
label,
|
|
policyOnly,
|
|
shouldLintFenceFiles,
|
|
version,
|
|
}) {
|
|
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 envVars = {
|
|
...getEnvironmentVariables({
|
|
buildTarget,
|
|
buildType,
|
|
version,
|
|
}),
|
|
...extraEnvironmentVariables,
|
|
};
|
|
setupBundlerDefaults(buildConfiguration, {
|
|
buildType,
|
|
envVars,
|
|
ignoredFiles,
|
|
policyOnly,
|
|
minify,
|
|
reloadOnChange,
|
|
shouldLintFenceFiles,
|
|
});
|
|
|
|
// 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,
|
|
buildType,
|
|
envVars,
|
|
ignoredFiles,
|
|
policyOnly,
|
|
minify,
|
|
reloadOnChange,
|
|
shouldLintFenceFiles,
|
|
},
|
|
) {
|
|
const { bundlerOpts } = buildConfiguration;
|
|
const extensions = ['.js', '.ts', '.tsx'];
|
|
|
|
Object.assign(bundlerOpts, {
|
|
// Source transforms
|
|
transform: [
|
|
// // Remove code that should be excluded from builds of the current type
|
|
createRemoveFencedCodeTransform(buildType, shouldLintFenceFiles),
|
|
// Transpile top-level code
|
|
[
|
|
babelify,
|
|
// Run TypeScript files through Babel
|
|
{ extensions },
|
|
],
|
|
// 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.DEVELOPMENT) {
|
|
bundlerOpts.manualIgnore.push('react-devtools');
|
|
bundlerOpts.manualIgnore.push('remote-redux-devtools');
|
|
}
|
|
|
|
// 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 });
|
|
}
|
|
}
|
|
|
|
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 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',
|
|
[],
|
|
'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);
|
|
});
|
|
}
|
|
// 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');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get environment variables to inject in the current build.
|
|
*
|
|
* @param {object} options - Build options.
|
|
* @param {BUILD_TARGETS} options.buildTarget - The current build target.
|
|
* @param {BuildType} options.buildType - The current build type (e.g. "main",
|
|
* "flask", etc.).
|
|
* @param {string} options.version - The current version of the extension.
|
|
* @returns {object} A map of environment variables to inject.
|
|
*/
|
|
function getEnvironmentVariables({ buildTarget, buildType, version }) {
|
|
const environment = getEnvironment({ buildTarget });
|
|
if (environment === ENVIRONMENT.PRODUCTION && !process.env.SENTRY_DSN) {
|
|
throw new Error('Missing SENTRY_DSN environment variable');
|
|
}
|
|
|
|
const devMode = isDevBuild(buildTarget);
|
|
const testing = isTestBuild(buildTarget);
|
|
return {
|
|
COLLECTIBLES_V1: metamaskrc.COLLECTIBLES_V1 === '1',
|
|
CONF: devMode ? metamaskrc : {},
|
|
IN_TEST: testing,
|
|
INFURA_PROJECT_ID: getInfuraProjectId({
|
|
buildType,
|
|
environment,
|
|
testing,
|
|
}),
|
|
METAMASK_DEBUG: devMode,
|
|
METAMASK_ENVIRONMENT: environment,
|
|
METAMASK_VERSION: version,
|
|
METAMASK_BUILD_TYPE: buildType,
|
|
NODE_ENV: devMode ? ENVIRONMENT.DEVELOPMENT : ENVIRONMENT.PRODUCTION,
|
|
ONBOARDING_V2: metamaskrc.ONBOARDING_V2 === '1',
|
|
PHISHING_WARNING_PAGE_URL: getPhishingWarningPageUrl({ testing }),
|
|
PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '',
|
|
PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '',
|
|
SEGMENT_HOST: metamaskrc.SEGMENT_HOST,
|
|
SEGMENT_WRITE_KEY: getSegmentWriteKey({ buildType, environment }),
|
|
SENTRY_DSN: process.env.SENTRY_DSN,
|
|
SENTRY_DSN_DEV: metamaskrc.SENTRY_DSN_DEV,
|
|
SIWE_V1: metamaskrc.SIWE_V1 === '1',
|
|
SWAPS_USE_DEV_APIS: process.env.SWAPS_USE_DEV_APIS === '1',
|
|
TOKEN_DETECTION_V2: metamaskrc.TOKEN_DETECTION_V2 === '1',
|
|
};
|
|
}
|
|
|
|
function getEnvironment({ buildTarget }) {
|
|
// get environment slug
|
|
if (isDevBuild(buildTarget)) {
|
|
return ENVIRONMENT.DEVELOPMENT;
|
|
} else if (isTestBuild(buildTarget)) {
|
|
return ENVIRONMENT.TESTING;
|
|
} else if (process.env.CIRCLE_BRANCH === 'master') {
|
|
return ENVIRONMENT.PRODUCTION;
|
|
} else if (
|
|
/^Version-v(\d+)[.](\d+)[.](\d+)/u.test(process.env.CIRCLE_BRANCH)
|
|
) {
|
|
return ENVIRONMENT.RELEASE_CANDIDATE;
|
|
} else if (process.env.CIRCLE_BRANCH === 'develop') {
|
|
return ENVIRONMENT.STAGING;
|
|
} else if (process.env.CIRCLE_PULL_REQUEST) {
|
|
return ENVIRONMENT.PULL_REQUEST;
|
|
}
|
|
return ENVIRONMENT.OTHER;
|
|
}
|
|
|
|
function renderHtmlFile({
|
|
htmlName,
|
|
groupSet,
|
|
commonSet,
|
|
browserPlatforms,
|
|
applyLavaMoat,
|
|
}) {
|
|
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 });
|
|
browserPlatforms.forEach((platform) => {
|
|
const dest = `./dist/${platform}/${htmlName}.html`;
|
|
// we dont have a way of creating async events atm
|
|
writeFileSync(dest, htmlOutput);
|
|
});
|
|
}
|
|
|
|
function beep() {
|
|
process.stdout.write('\x07');
|
|
}
|
|
|
|
function gracefulError(err) {
|
|
console.warn(err);
|
|
beep();
|
|
}
|