react-native-code-push-diff
Version:
This library builds upon the foundational strengths of the react-native-code-push library, adding advanced functionality to precisely identify and manage differences between code-push builds.
257 lines (222 loc) • 7.64 kB
text/typescript
import path from 'path'
import * as fs from 'fs'
import { tmpdir } from 'os'
import * as childProcess from 'child_process'
import { info, rmRf, execCommand, buildBundleConfig, mkdir } from './utils'
import type { BundleArgs, BundlerConfig, Hashes } from './types'
import { checkout, gitRestore } from './git'
import { hashes, removeUnchangedAssets } from './diff'
import { metroBundle } from './metroBundle'
export async function bundle(args: BundleArgs) {
const { base } = args
const bundlerConfig = buildBundleConfig(args)
const baseHashes = await readBaseHashes(bundlerConfig, base)
info('Bundling...')
process.env.CODE_PUSH_PLATFORM = bundlerConfig.os
const currentOutput = await bundleReactNative(
{
...bundlerConfig,
extraBundlerOptions: [
...(bundlerConfig.extraBundlerOptions ?? []),
'--config',
path.join(__dirname, 'metro.config.js'),
],
},
true
)
const currentHashes = await hashes(currentOutput.outputDir)
info('Removing unchanged assets...')
await removeUnchangedAssets(bundlerConfig.outputDir, currentHashes, baseHashes)
info('Bundling: ✔')
return currentOutput
}
const bundleReactNative = async (config: BundlerConfig, shouldBuildSourceMaps?: boolean) => {
const {
bundleCommand,
bundleName,
entryFile,
os,
extraBundlerOptions = [],
sourcemapOutputDir,
sourcemapOutput = path.join(sourcemapOutputDir, bundleName + '.map'),
extraHermesFlags = [],
useHermes = true,
outputDir,
development,
} = config
rmRf(outputDir)
mkdir(outputDir)
mkdir(path.dirname(sourcemapOutput))
info(`Using '${config.reinstallNodeModulesCommand}' to install node modules`)
await execCommand(config.reinstallNodeModulesCommand)
metroBundle({
bundleCommand,
bundleName,
development,
entryFile,
outputDir,
platform: os,
sourcemapOutput,
extraBundlerOptions,
verbose: useHermes,
minify: !useHermes,
})
if (shouldBuildSourceMaps && useHermes) {
await runHermesEmitBinaryCommand(bundleName, outputDir, sourcemapOutput, [
'-max-diagnostic-width=80',
...extraHermesFlags,
])
}
return {
outputDir,
bundlePath: path.join(outputDir, bundleName),
sourcemap: sourcemapOutput,
}
}
const checkoutAndBuild = async (bundlerConfig: BundlerConfig, commit: string) => {
await checkout(commit)
const output = await bundleReactNative(bundlerConfig, false)
await gitRestore()
return output
}
const readBaseHashes = async (bundlerConfig: BundlerConfig, base: string): Promise<Hashes> => {
if (!process.env.BASE_ASSETS_PATH) {
info(`Bundling for ${base}`)
const baseOutput = await checkoutAndBuild(bundlerConfig, base)
const baseHashes = await hashes(baseOutput.outputDir)
const baseAssetsPath = path.join(tmpdir(), 'base-assets.json')
fs.writeFileSync(baseAssetsPath, JSON.stringify(baseHashes))
process.env.BASE_ASSETS_PATH = baseAssetsPath
}
return JSON.parse(fs.readFileSync(process.env.BASE_ASSETS_PATH, 'utf8'))
}
export async function runHermesEmitBinaryCommand(
bundleName: string,
outputFolder: string,
sourcemapOutput: string,
extraHermesFlags: string[]
): Promise<void> {
const hermesArgs: string[] = []
Array.prototype.push.apply(hermesArgs, [
'-emit-binary',
'-out',
path.join(outputFolder, bundleName + '.hbc'),
path.join(outputFolder, bundleName),
...extraHermesFlags,
])
if (sourcemapOutput) {
hermesArgs.push('-output-source-map')
}
hermesArgs.push('-w')
await buildHermes(bundleName, outputFolder, hermesArgs)
if (sourcemapOutput) {
await composeSourceMaps(bundleName, outputFolder, sourcemapOutput)
}
}
async function buildHermes(bundleName: string, outputFolder: string, hermesArgs: string[]) {
const hermesCommand = getHermesCommand()
const hermesProcess = childProcess.spawn(hermesCommand, hermesArgs)
return new Promise<void>((resolve, reject) => {
hermesProcess.stdout.on('data', (data: Buffer) => {
console.log(data.toString().trim())
})
hermesProcess.stderr.on('data', (data: Buffer) => {
console.error(data.toString().trim())
})
hermesProcess.on('close', (exitCode: number, signal: string) => {
if (exitCode !== 0) {
reject(new Error(`"hermes" command failed (exitCode=${exitCode}, signal=${signal}).`))
}
// Copy HBC bundle to overwrite JS bundle
const source = path.join(outputFolder, bundleName + '.hbc')
const destination = path.join(outputFolder, bundleName)
fs.copyFile(source, destination, (err) => {
if (err) {
console.error(err)
reject(
new Error(
`Copying file ${source} to ${destination} failed. "hermes" previously exited with code ${exitCode}.`
)
)
}
fs.unlink(source, (error) => {
if (error) {
console.error(error)
reject(error)
}
resolve()
})
})
})
})
}
async function composeSourceMaps(bundleName: string, outputFolder: string, sourcemapOutput: string) {
const composeSourceMapsPath = getComposeSourceMapsPath()
if (!composeSourceMapsPath) {
throw new Error('react-native compose-source-maps.js scripts is not found')
}
const jsCompilerSourceMapFile = path.join(outputFolder, bundleName + '.hbc' + '.map')
if (!fs.existsSync(jsCompilerSourceMapFile)) {
throw new Error(`sourcemap file ${jsCompilerSourceMapFile} is not found`)
}
return new Promise((resolve, reject) => {
const composeSourceMapsArgs = [
composeSourceMapsPath,
sourcemapOutput,
jsCompilerSourceMapFile,
'-o',
sourcemapOutput,
]
// https://github.com/facebook/react-native/blob/master/react.gradle#L211
// https://github.com/facebook/react-native/blob/master/scripts/react-native-xcode.sh#L178
// packager.sourcemap.map + hbc.sourcemap.map = sourcemap.map
const composeSourceMapsProcess = childProcess.spawn('node', composeSourceMapsArgs)
console.log(`${composeSourceMapsPath} ${composeSourceMapsArgs.join(' ')}`)
composeSourceMapsProcess.stdout.on('data', (data: Buffer) => {
console.log(data.toString().trim())
})
composeSourceMapsProcess.stderr.on('data', (data: Buffer) => {
console.error(data.toString().trim())
})
composeSourceMapsProcess.on('close', (exitCode: number, signal: string) => {
if (exitCode !== 0) {
reject(new Error(`"compose-source-maps" command failed (exitCode=${exitCode}, signal=${signal}).`))
}
// Delete the HBC sourceMap, otherwise it will be included in 'code-push' bundle as well
fs.unlink(jsCompilerSourceMapFile, (err) => {
if (err) {
console.error(err)
reject(err)
}
resolve(null)
})
})
})
}
function getHermesCommand() {
return `${getReactNativePackagePath()}/sdks/hermesc/${getHermesOSBin()}/hermesc`
}
function getReactNativePackagePath() {
return path.join('node_modules', 'react-native')
}
function getHermesOSBin() {
switch (process.platform) {
case 'win32':
return 'win64-bin'
case 'darwin':
return 'osx-bin'
case 'freebsd':
case 'linux':
case 'sunos':
default:
return 'linux64-bin'
}
}
function getComposeSourceMapsPath() {
// detect if compose-source-maps.js script exists
const composeSourceMapsPath = path.join(getReactNativePackagePath(), 'scripts', 'compose-source-maps.js')
if (fs.existsSync(composeSourceMapsPath)) {
return composeSourceMapsPath
}
return null
}