react-native-bundle-visualizer
Version:
See what's inside your react-native bundle
218 lines (202 loc) • 6.19 kB
JavaScript
const fs = require('node:fs');
const os = require('node:os');
const process = require('node:process');
const path = require('node:path');
const chalk = require('chalk');
const argv = require('minimist')(process.argv.slice(2));
const {execa} = require('execa');
const open = require('open').default;
const {explore} = require('source-map-explorer');
const pkgJSON = JSON.parse(fs.readFileSync('./package.json'));
function sanitizeString(str) {
return str ? str.replace(/[^\w]/gi, '') : str;
}
function getAppName() {
if (pkgJSON.name) return sanitizeString(pkgJSON.name);
try {
const appJSON = JSON.parse(fs.readFileSync('./app.json'));
return (
sanitizeString(appJSON.name) ||
sanitizeString(appJSON.expo.name) ||
'UnknownApp'
);
} catch (err) {
return 'UnknownApp';
}
}
function getEntryPoint() {
let entry = pkgJSON.main || 'index.js';
if (entry[0] !== '.' && entry[0] !== '/' && entry[0] !== '\\') {
entry = './' + entry;
}
return entry;
}
function getReactNativeBin() {
const localBin = isExpo ? 'node_modules/.bin/expo' : './node_modules/.bin/react-native';
if (fs.existsSync(localBin)) return localBin;
try {
const reactNativeDir = path.dirname(
require.resolve('react-native/package.json')
);
return path.join(reactNativeDir, './cli.js');
} catch (e) {
console.error(
chalk.red.bold(
`React-native binary could not be located. Please report this issue with environment info to:\n`
),
chalk.blue.bold(`-> ${require('../package.json').bugs}`)
);
}
}
// Get (default) arguments
const baseDir = path.join(os.tmpdir(), 'react-native-bundle-visualizer');
const tmpDir = path.join(baseDir, getAppName());
const outDir = path.join(tmpDir, 'output');
const entryFile = argv['entry-file'] || getEntryPoint();
const platform = argv.platform || 'ios';
const isExpo = argv.expo || false;
const dev = argv.dev || false;
const verbose = argv.verbose || false;
const resetCache = argv['reset-cache'] || false;
let bundleOutput =
argv['bundle-output'] || path.join(tmpDir, platform + '.bundle');
let bundleOutputSourceMap = bundleOutput + '.map';
const format = argv.format || 'html';
const bundleOutputExplorerFile = path.join(outDir, 'explorer.' + format);
const onlyMapped = !!argv['only-mapped'] || false;
const borderChecks = argv['border-checks'] === true;
const errorOnFail = argv['error-on-fail'] || false;
// Make sure the temp dir exists
fs.mkdirSync(baseDir, { recursive: true });
fs.mkdirSync(tmpDir, { recursive: true });
// Try to obtain the previous file size
let prevBundleSize;
if (fs.existsSync(bundleOutput)) {
const stats = fs.statSync(bundleOutput);
prevBundleSize = stats.size;
}
// Bundle
const expoOutputDir = path.parse(bundleOutput).dir;
console.log(chalk.green.bold('Generating bundle...'));
const commands = isExpo ? [
'export',
'--platform',
platform,
dev && '--dev',
'--output-dir',
expoOutputDir,
'--no-bytecode',
'--source-maps',
].filter(Boolean) : [
'bundle',
'--platform',
platform,
'--dev',
dev,
'--entry-file',
entryFile,
'--bundle-output',
bundleOutput,
'--sourcemap-output',
bundleOutputSourceMap,
'--minify',
isExpo,
];
if (resetCache) {
if (isExpo) {
commands.push('--clear');
} else {
commands.push('--reset-cache');
commands.push(resetCache);
}
}
const reactNativeBin = getReactNativeBin();
const bundlePromise = execa(reactNativeBin, commands);
bundlePromise.stdout.pipe(process.stdout);
// Upon bundle completion, run `source-map-explorer`
bundlePromise
.then(
() => {
if (isExpo) {
const jsFolder = `${expoOutputDir}/_expo/static/js/${platform}`;
const files = fs.readdirSync(`${expoOutputDir}/_expo/static/js/${platform}`);
bundleOutput = jsFolder + '/' + files.find((file) => file.endsWith('.js'));
bundleOutputSourceMap = jsFolder + '/' + files.find((file) => file.endsWith('.js.map'));
}
// Log bundle-size
const stats = fs.statSync(bundleOutput);
// Log increase or decrease since last run
let deltaSuffix = '';
if (prevBundleSize) {
const delta = stats.size - prevBundleSize;
if (delta > 0) {
deltaSuffix = chalk.yellow(
' (+++ has increased with ' + delta + ' bytes since last run)'
);
} else if (delta < 0) {
deltaSuffix = chalk.green.bold(
' (--- has decreased with ' + (0 - delta) + ' bytes since last run)'
);
} else {
deltaSuffix = chalk.green(' (unchanged since last run)');
}
}
console.log(
chalk.green.bold(
'Bundle is ' +
Math.round((stats.size / (1024 * 1024)) * 100) / 100 +
' MB in size'
) + deltaSuffix
);
// Make sure the explorer output dir is removed
fs.rmSync(outDir, { recursive: true, force: true });
return explore(
{
code: bundleOutput,
map: bundleOutputSourceMap,
},
{
onlyMapped,
noRoot: false,
noBorderChecks: borderChecks === false,
output: {
format,
filename: bundleOutputExplorerFile,
},
}
);
}
// Log info and open output file
)
.then((result) => {
if (verbose) {
result.bundles.forEach((bundle) => {
Object.keys(bundle.files).forEach((file) => {
console.log(
chalk.green(file + ', size: ' + bundle.files[file].size + ' bytes')
);
});
});
}
// Log any errors
if (result.errors) {
result.errors.forEach((error) => {
if (error.isWarning) {
console.log(chalk.yellow.bold(error.message));
} else {
console.log(chalk.red.bold(error.message));
}
});
}
console.log(
chalk.green.bold('Opening bundle visualizer output file: ' + bundleOutputExplorerFile)
)
return open(bundleOutputExplorerFile);
})
.catch((error) => {
console.log(chalk.red('=== error ==='), error)
if (errorOnFail) {
process.exit(1);
}
});