Skip to content

Commit 5b83c97

Browse files
authored
Merge pull request Soomgo-Mobile#35 from Soomgo-Mobile/feature/issue-18
Implement CodePush Bundle Generation Command
2 parents 981cdc0 + 7b445c3 commit 5b83c97

13 files changed

+797
-8
lines changed

cli/commands/bundleCodePush.js

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const { prepareToBundleJS } = require('../functions/prepareToBundleJS');
2+
const { runReactNativeBundleCommand } = require('../functions/runReactNativeBundleCommand');
3+
const { getReactTempDir } = require('../functions/getReactTempDir');
4+
const { runHermesEmitBinaryCommand } = require('../functions/runHermesEmitBinaryCommand');
5+
const { makeCodePushBundle } = require('../functions/makeCodePushBundle');
6+
7+
/**
8+
* @param platform {string} 'ios' | 'android'
9+
* @param outputRootPath {string}
10+
* @param entryFile {string}
11+
* @param bundleName {string|undefined}
12+
* @return {Promise<{bundleFileName: string, packageHash: string}>}
13+
*/
14+
async function runBundleCodePush(
15+
platform = 'ios',
16+
outputRootPath = 'build',
17+
entryFile = 'index.ts',
18+
bundleName,
19+
) {
20+
const OUTPUT_CONTENT_PATH = `${outputRootPath}/CodePush`;
21+
const BUNDLE_NAME_DEFAULT = platform === 'ios' ? 'main.jsbundle' : 'index.android.bundle';
22+
const _bundleName = bundleName ? bundleName : BUNDLE_NAME_DEFAULT;
23+
const SOURCEMAP_OUTPUT = `${outputRootPath}/${_bundleName}.map`;
24+
25+
prepareToBundleJS({ deleteDirs: [outputRootPath, getReactTempDir()], makeDir: OUTPUT_CONTENT_PATH });
26+
27+
runReactNativeBundleCommand(
28+
_bundleName,
29+
OUTPUT_CONTENT_PATH,
30+
platform,
31+
SOURCEMAP_OUTPUT,
32+
entryFile,
33+
);
34+
console.log('log: JS bundling complete');
35+
36+
await runHermesEmitBinaryCommand(
37+
_bundleName,
38+
OUTPUT_CONTENT_PATH,
39+
SOURCEMAP_OUTPUT,
40+
);
41+
console.log('log: Hermes compilation complete');
42+
43+
const { bundleFileName, packageHash } = await makeCodePushBundle(OUTPUT_CONTENT_PATH);
44+
console.log(`log: CodePush bundle created (file name: ${bundleFileName})`);
45+
46+
return { bundleFileName, packageHash }; // returns for release command implementation
47+
}
48+
49+
module.exports = { runBundleCodePush };

cli/functions/getReactTempDir.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* code based on appcenter-cli
3+
*/
4+
5+
const os = require('os');
6+
7+
/**
8+
* Return the path of the temporary directory for react-native bundling
9+
*
10+
* @return {string}
11+
*/
12+
function getReactTempDir() {
13+
return `${os.tmpdir()}/react-*`;
14+
}
15+
16+
module.exports = { getReactTempDir };

cli/functions/makeCodePushBundle.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const { randomUUID } = require('crypto');
2+
const path = require('path');
3+
const shell = require('shelljs');
4+
const zip = require('../utils/zip');
5+
const {generatePackageHashFromDirectory} = require('../utils/hash-utils');
6+
7+
/**
8+
* Create a CodePush bundle file and return the information.
9+
*
10+
* @param contentsPath {string} The directory path containing the contents to be made into a CodePush bundle (usually the 'build/CodePush' directory))
11+
* @return {Promise<{ bundleFileName: string, packageHash: string }>}
12+
*/
13+
async function makeCodePushBundle(contentsPath) {
14+
const updateContentsZipPath = await zip(contentsPath);
15+
16+
const bundleFileName = randomUUID();
17+
shell.mv(updateContentsZipPath, `./${bundleFileName}`);
18+
19+
const packageHash = await generatePackageHashFromDirectory(contentsPath, path.join(contentsPath, '..'));
20+
21+
return {
22+
bundleFileName: bundleFileName,
23+
packageHash: packageHash,
24+
};
25+
}
26+
27+
module.exports = { makeCodePushBundle };

cli/functions/prepareToBundleJS.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const shell = require('shelljs');
2+
3+
/**
4+
* @param deleteDirs {string[]} Directories to delete
5+
* @param makeDir {string} Directory path to create
6+
*/
7+
function prepareToBundleJS({ deleteDirs, makeDir }) {
8+
shell.rm('-rf', deleteDirs);
9+
shell.mkdir('-p', makeDir);
10+
}
11+
12+
module.exports = { prepareToBundleJS };
+213
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/**
2+
* code based on appcenter-cli
3+
*/
4+
5+
const childProcess = require('child_process');
6+
const fs = require('fs');
7+
const path = require('path');
8+
const shell = require('shelljs');
9+
10+
/**
11+
* Run Hermes compile CLI command
12+
*
13+
* @param bundleName {string} JS bundle file name
14+
* @param outputPath {string} Path to output .hbc file
15+
* @param sourcemapOutput {string} Path to output sourcemap file (Warning: if sourcemapOutput points to the outputPath, the sourcemap will be included in the CodePush bundle and increase the deployment size)
16+
* @param extraHermesFlags {string[]} Additional options to pass to `hermesc` command
17+
* @return {Promise<void>}
18+
*/
19+
async function runHermesEmitBinaryCommand(
20+
bundleName,
21+
outputPath,
22+
sourcemapOutput,
23+
extraHermesFlags = [],
24+
) {
25+
/**
26+
* @type {string[]}
27+
*/
28+
const hermesArgs = [
29+
'-emit-binary',
30+
'-out',
31+
path.join(outputPath, bundleName + '.hbc'),
32+
path.join(outputPath, bundleName),
33+
...extraHermesFlags,
34+
];
35+
if (sourcemapOutput) {
36+
hermesArgs.push('-output-source-map');
37+
}
38+
39+
console.log('Converting JS bundle to byte code via Hermes, running command:\n');
40+
41+
return new Promise((resolve, reject) => {
42+
try {
43+
const hermesCommand = getHermesCommand();
44+
45+
const disableAllWarningsArg = '-w';
46+
shell.exec(`${hermesCommand} ${hermesArgs.join(' ')} ${disableAllWarningsArg}`);
47+
48+
// Copy HBC bundle to overwrite JS bundle
49+
const source = path.join(outputPath, bundleName + '.hbc');
50+
const destination = path.join(outputPath, bundleName);
51+
shell.cp(source, destination);
52+
shell.rm(source);
53+
resolve();
54+
} catch (e) {
55+
reject(e);
56+
}
57+
}).then(() => {
58+
if (!sourcemapOutput) {
59+
// skip source map compose if source map is not enabled
60+
return;
61+
}
62+
63+
// compose-source-maps.js file path
64+
const composeSourceMapsPath = getComposeSourceMapsPath();
65+
if (composeSourceMapsPath === null) {
66+
throw new Error('react-native compose-source-maps.js scripts is not found');
67+
}
68+
69+
const jsCompilerSourceMapFile = path.join(outputPath, bundleName + '.hbc' + '.map');
70+
if (!fs.existsSync(jsCompilerSourceMapFile)) {
71+
throw new Error(`sourcemap file ${jsCompilerSourceMapFile} is not found`);
72+
}
73+
74+
return new Promise((resolve, reject) => {
75+
const composeSourceMapsArgs = [
76+
composeSourceMapsPath,
77+
sourcemapOutput,
78+
jsCompilerSourceMapFile,
79+
'-o',
80+
sourcemapOutput,
81+
];
82+
const composeSourceMapsProcess = childProcess.spawn('node', composeSourceMapsArgs);
83+
console.log(`${composeSourceMapsPath} ${composeSourceMapsArgs.join(' ')}`);
84+
85+
composeSourceMapsProcess.stdout.on('data', (data) => {
86+
console.log(data.toString().trim());
87+
});
88+
89+
composeSourceMapsProcess.stderr.on('data', (data) => {
90+
console.error(data.toString().trim());
91+
});
92+
93+
composeSourceMapsProcess.on('close', (exitCode, signal) => {
94+
if (exitCode !== 0) {
95+
reject(new Error(`"compose-source-maps" command failed (exitCode=${exitCode}, signal=${signal}).`));
96+
}
97+
98+
// Delete the HBC sourceMap, otherwise it will be included in 'code-push' bundle as well
99+
fs.unlink(jsCompilerSourceMapFile, (err) => {
100+
if (err != null) {
101+
console.error(err);
102+
reject(err);
103+
}
104+
105+
resolve();
106+
});
107+
});
108+
});
109+
});
110+
}
111+
112+
/**
113+
* @return {string}
114+
*/
115+
function getHermesCommand() {
116+
/**
117+
* @type {(file: string) => boolean}
118+
*/
119+
const fileExists = (file) => {
120+
try {
121+
return fs.statSync(file).isFile();
122+
} catch (e) {
123+
return false;
124+
}
125+
};
126+
// Hermes is bundled with react-native since 0.69
127+
const bundledHermesEngine = path.join(
128+
getReactNativePackagePath(),
129+
'sdks',
130+
'hermesc',
131+
getHermesOSBin(),
132+
getHermesOSExe(),
133+
);
134+
if (fileExists(bundledHermesEngine)) {
135+
return bundledHermesEngine;
136+
}
137+
throw new Error('Hermes engine binary not found. Please upgrade to react-native 0.69 or later');
138+
}
139+
140+
/**
141+
* @return {string}
142+
*/
143+
function getHermesOSBin() {
144+
switch (process.platform) {
145+
case 'win32':
146+
return 'win64-bin';
147+
case 'darwin':
148+
return 'osx-bin';
149+
case 'freebsd':
150+
case 'linux':
151+
case 'sunos':
152+
default:
153+
return 'linux64-bin';
154+
}
155+
}
156+
157+
/**
158+
* @return {string}
159+
*/
160+
function getHermesOSExe() {
161+
const hermesExecutableName = 'hermesc';
162+
switch (process.platform) {
163+
case 'win32':
164+
return hermesExecutableName + '.exe';
165+
default:
166+
return hermesExecutableName;
167+
}
168+
}
169+
170+
/**
171+
* @return {string | null}
172+
*/
173+
function getComposeSourceMapsPath() {
174+
// detect if compose-source-maps.js script exists
175+
const composeSourceMaps = path.join(getReactNativePackagePath(), 'scripts', 'compose-source-maps.js');
176+
if (fs.existsSync(composeSourceMaps)) {
177+
return composeSourceMaps;
178+
}
179+
return null;
180+
}
181+
182+
/**
183+
* @return {string}
184+
*/
185+
function getReactNativePackagePath() {
186+
const result = childProcess.spawnSync('node', [
187+
'--print',
188+
"require.resolve('react-native/package.json')",
189+
]);
190+
const packagePath = path.dirname(result.stdout.toString());
191+
if (result.status === 0 && directoryExistsSync(packagePath)) {
192+
return packagePath;
193+
}
194+
195+
return path.join('node_modules', 'react-native');
196+
}
197+
198+
/**
199+
* @param dirname {string}
200+
* @return {boolean}
201+
*/
202+
function directoryExistsSync(dirname) {
203+
try {
204+
return fs.statSync(dirname).isDirectory();
205+
} catch (err) {
206+
if (err.code !== 'ENOENT') {
207+
throw err;
208+
}
209+
}
210+
return false;
211+
}
212+
213+
module.exports = { runHermesEmitBinaryCommand };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* code based on appcenter-cli
3+
*/
4+
5+
const path = require('path');
6+
const shell = require('shelljs');
7+
8+
/**
9+
* Run `react-native bundle` CLI command
10+
*
11+
* @param bundleName {string} JS bundle file name
12+
* @param entryFile {string} App code entry file name (default: index.ts)
13+
* @param outputPath {string} Path to output JS bundle file and assets
14+
* @param platform {string} Platform (ios | android)
15+
* @param sourcemapOutput {string} Path to output sourcemap file (Warning: if sourcemapOutput points to the outputPath, the sourcemap will be included in the CodePush bundle and increase the deployment size)
16+
* @param extraBundlerOptions {string[]} Additional options to pass to `react-native bundle` command
17+
* @return {void}
18+
*/
19+
function runReactNativeBundleCommand(
20+
bundleName,
21+
outputPath,
22+
platform,
23+
sourcemapOutput,
24+
entryFile = 'index.ts',
25+
extraBundlerOptions = [],
26+
) {
27+
/**
28+
* @return {string}
29+
*/
30+
function getCliPath() {
31+
return path.join('node_modules', '.bin', 'react-native');
32+
}
33+
34+
/**
35+
* @type {string[]}
36+
*/
37+
const reactNativeBundleArgs = [
38+
'bundle',
39+
'--assets-dest',
40+
outputPath,
41+
'--bundle-output',
42+
path.join(outputPath, bundleName),
43+
'--dev',
44+
'false',
45+
'--entry-file',
46+
entryFile,
47+
'--platform',
48+
platform,
49+
'--sourcemap-output',
50+
sourcemapOutput,
51+
...extraBundlerOptions,
52+
];
53+
54+
console.log('Running "react-native bundle" command:\n');
55+
56+
shell.exec(`${getCliPath()} ${reactNativeBundleArgs.join(' ')}`);
57+
}
58+
59+
module.exports = { runReactNativeBundleCommand };

0 commit comments

Comments
 (0)