Skip to content

Commit 37dbd45

Browse files
authored
Merge pull request #12 from sweatco/code-push-standalone
refactor: update CLI arguments for code-push-standalone support
2 parents 09701f8 + 62d5560 commit 37dbd45

12 files changed

Lines changed: 472 additions & 106 deletions

File tree

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { name as appName } from './app.json'
2121
AppRegistry.registerComponent(appName, () => App)
2222
```
2323

24-
2. Run the `code-push-diff release-react --app {YOUR_APP_NAME} --base {BRANCH_OR_COMMIT}` if you need to create and release a codepush bundle.
24+
2. Run the `code-push-diff release-react android --app {YOUR_APP_NAME} --base {BRANCH_OR_COMMIT} --cmd code-push-standalone` if you need to create and release a codepush bundle.
2525

2626
## How does it work?
2727
The `bundle` or `release-react` scripts initiate their process by generating bundles corresponding to the current active `git commit` and a specified `base` commit, which is provided as an argument. This step is crucial for identifying the exact changes in assets that have occurred between these two states. Following this, the scripts compute the differences in assets across the generated bundles to pinpoint which files have been modified or added.
@@ -34,36 +34,40 @@ The `react-native-code-push-diff/setup` plays a key role by setting up a custom
3434

3535
The `bundle` command is designed to generate a codepush bundle incorporating only the assets that have changed:
3636
```sh
37-
yarn code-push-diff bundle {appOrOs} --base {branchOrCommit}
37+
yarn code-push-diff bundle {platform} --base {branchOrCommit}
3838
```
3939

40-
- `appOrOs` - this parameter specifies the context in which the bundle is to be built. You can either input the name of an application as defined within your appcenter configuration or directly specify the platform for which you are building the bundle (`ios` or `android`).
40+
- `platform` - specifies the platform for which you are building the bundle (`ios` or `android`).
4141

4242
- `branchOrCommit` - this argument determines the reference point for calculating asset differences. It should be the identifier of the branch or commit that served as the basis for your most recent application build. By specifying this, the script can accurately assess which assets have changed and need to be included in the codepush bundle, ensuring that your updates are both efficient and relevant.
4343

4444

4545
For example:
46-
`code-push-diff bundle i.kuchaev/AwesomeProject --base release-2.1`
46+
`code-push-diff bundle android --base release-2.1`
4747

4848
Also the script apply all args for the [release-react](https://github.com/microsoft/code-push/tree/v3.0.1/cli#releasing-updates-react-native) command, like `plist-file`, `output-dir` etc.
4949

5050
For example:
51-
`code-push-diff bundle i.kuchaev/AwesomeProject --base release-2.1 --plist-file ios/project/Info.plist`
51+
`code-push-diff bundle android --base release-2.1 --plist-file ios/project/Info.plist`
5252

5353
### release-react
5454
The `release-react` command leverages the code-push-diff bundle functionality to construct a bundle and then releases it to App Center:
5555
```sh
56-
code-push-diff release-react --app {appName} --base {branchOrCommit}
56+
code-push-diff release-react {platform} --app {appName} --base {branchOrCommit} --cmd code-push-standalone
5757
```
5858

59+
- `platform` - specifies the platform for which you are building the bundle (`ios` or `android`).
60+
5961
- `app` - This should be the exact name of your application as registered in App Center. It enables the script to correctly identify and target the application for the update release.
6062

6163
- `branchOrCommit` - Specify the branch or commit that will act as a reference point for identifying changes. Typically, you should use the commit or branch that corresponds to the last build of your released application. This comparison helps in determining which assets have changed and need to be included in the release.
6264

65+
- `cmd` - command to release the bundle to a code push server.
66+
6367
This script also supports all the parameters available for the [release-react command](https://github.com/microsoft/code-push/tree/v3.0.1/cli#releasing-updates-react-native) used by CodePush, , like `plistFile`, `outputDir` etc.
6468

6569
```sh
66-
code-push-diff release-react --app i.kuchaev/AwesomeProject --base release-2.1 --plist-file ios/project/Info.plist --rallout 50
70+
code-push-diff release-react ios --app i.kuchaev/AwesomeProject --base release-2.1 --plist-file ios/project/Info.plist --rallout 50 --cmd code-push-standalone
6771
```
6872

6973
## Contributing

example/scripts/codepush.js

Lines changed: 0 additions & 17 deletions
This file was deleted.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
"@types/jest": "^28.1.2",
6969
"@types/react": "~17.0.21",
7070
"@types/react-native": "0.70.0",
71-
"appcenter-cli": "^2.14.0",
7271
"commitlint": "^17.0.2",
7372
"del-cli": "^5.0.0",
7473
"eslint": "^8.4.1",
@@ -88,7 +87,6 @@
8887
"@types/react": "17.0.21"
8988
},
9089
"peerDependencies": {
91-
"appcenter-cli": "*",
9290
"react": "*",
9391
"react-native": "*"
9492
},
@@ -153,7 +151,10 @@
153151
]
154152
},
155153
"dependencies": {
156-
"simple-git": "^3.22.0",
154+
"gradle-to-js": "2.0.1",
155+
"plist": "^3.0.6",
156+
"semver": "^7.5.3",
157+
"xcode": "^3.0.1",
157158
"yargs": "^17.7.2"
158159
}
159160
}

src/bundle/bundle.ts

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import path from 'path'
22
import * as fs from 'fs'
33
import { tmpdir } from 'os'
4+
import * as childProcess from 'child_process'
45

56
import { info, rmRf, execCommand, buildBundleConfig, mkdir } from './utils'
67
import type { BundleArgs, BundlerConfig, Hashes } from './types'
78
import { checkout, gitRestore } from './git'
89
import { hashes, removeUnchangedAssets } from './diff'
910
import { metroBundle } from './metroBundle'
1011

11-
const { runHermesEmitBinaryCommand } = require('appcenter-cli/dist/commands/codepush/lib/react-native-utils')
12-
1312
export async function bundle(args: BundleArgs) {
1413
const { base } = args
1514
const bundlerConfig = buildBundleConfig(args)
@@ -104,3 +103,154 @@ const readBaseHashes = async (bundlerConfig: BundlerConfig, base: string): Promi
104103

105104
return JSON.parse(fs.readFileSync(process.env.BASE_ASSETS_PATH, 'utf8'))
106105
}
106+
107+
export async function runHermesEmitBinaryCommand(
108+
bundleName: string,
109+
outputFolder: string,
110+
sourcemapOutput: string,
111+
extraHermesFlags: string[]
112+
): Promise<void> {
113+
const hermesArgs: string[] = []
114+
115+
Array.prototype.push.apply(hermesArgs, [
116+
'-emit-binary',
117+
'-out',
118+
path.join(outputFolder, bundleName + '.hbc'),
119+
path.join(outputFolder, bundleName),
120+
...extraHermesFlags,
121+
])
122+
123+
if (sourcemapOutput) {
124+
hermesArgs.push('-output-source-map')
125+
}
126+
127+
hermesArgs.push('-w')
128+
129+
await buildHermes(bundleName, outputFolder, hermesArgs)
130+
131+
if (sourcemapOutput) {
132+
await composeSourceMaps(bundleName, outputFolder, sourcemapOutput)
133+
}
134+
}
135+
136+
async function buildHermes(bundleName: string, outputFolder: string, hermesArgs: string[]) {
137+
const hermesCommand = getHermesCommand()
138+
const hermesProcess = childProcess.spawn(hermesCommand, hermesArgs)
139+
return new Promise<void>((resolve, reject) => {
140+
hermesProcess.stdout.on('data', (data: Buffer) => {
141+
console.log(data.toString().trim())
142+
})
143+
144+
hermesProcess.stderr.on('data', (data: Buffer) => {
145+
console.error(data.toString().trim())
146+
})
147+
148+
hermesProcess.on('close', (exitCode: number, signal: string) => {
149+
if (exitCode !== 0) {
150+
reject(new Error(`"hermes" command failed (exitCode=${exitCode}, signal=${signal}).`))
151+
}
152+
// Copy HBC bundle to overwrite JS bundle
153+
const source = path.join(outputFolder, bundleName + '.hbc')
154+
const destination = path.join(outputFolder, bundleName)
155+
fs.copyFile(source, destination, (err) => {
156+
if (err) {
157+
console.error(err)
158+
reject(
159+
new Error(
160+
`Copying file ${source} to ${destination} failed. "hermes" previously exited with code ${exitCode}.`
161+
)
162+
)
163+
}
164+
fs.unlink(source, (error) => {
165+
if (error) {
166+
console.error(error)
167+
reject(error)
168+
}
169+
resolve()
170+
})
171+
})
172+
})
173+
})
174+
}
175+
176+
async function composeSourceMaps(bundleName: string, outputFolder: string, sourcemapOutput: string) {
177+
const composeSourceMapsPath = getComposeSourceMapsPath()
178+
if (!composeSourceMapsPath) {
179+
throw new Error('react-native compose-source-maps.js scripts is not found')
180+
}
181+
const jsCompilerSourceMapFile = path.join(outputFolder, bundleName + '.hbc' + '.map')
182+
if (!fs.existsSync(jsCompilerSourceMapFile)) {
183+
throw new Error(`sourcemap file ${jsCompilerSourceMapFile} is not found`)
184+
}
185+
186+
return new Promise((resolve, reject) => {
187+
const composeSourceMapsArgs = [
188+
composeSourceMapsPath,
189+
sourcemapOutput,
190+
jsCompilerSourceMapFile,
191+
'-o',
192+
sourcemapOutput,
193+
]
194+
195+
// https://github.com/facebook/react-native/blob/master/react.gradle#L211
196+
// https://github.com/facebook/react-native/blob/master/scripts/react-native-xcode.sh#L178
197+
// packager.sourcemap.map + hbc.sourcemap.map = sourcemap.map
198+
const composeSourceMapsProcess = childProcess.spawn('node', composeSourceMapsArgs)
199+
console.log(`${composeSourceMapsPath} ${composeSourceMapsArgs.join(' ')}`)
200+
201+
composeSourceMapsProcess.stdout.on('data', (data: Buffer) => {
202+
console.log(data.toString().trim())
203+
})
204+
205+
composeSourceMapsProcess.stderr.on('data', (data: Buffer) => {
206+
console.error(data.toString().trim())
207+
})
208+
209+
composeSourceMapsProcess.on('close', (exitCode: number, signal: string) => {
210+
if (exitCode !== 0) {
211+
reject(new Error(`"compose-source-maps" command failed (exitCode=${exitCode}, signal=${signal}).`))
212+
}
213+
214+
// Delete the HBC sourceMap, otherwise it will be included in 'code-push' bundle as well
215+
fs.unlink(jsCompilerSourceMapFile, (err) => {
216+
if (err) {
217+
console.error(err)
218+
reject(err)
219+
}
220+
221+
resolve(null)
222+
})
223+
})
224+
})
225+
}
226+
227+
function getHermesCommand() {
228+
return `${getReactNativePackagePath()}/sdks/hermesc/${getHermesOSBin()}/hermesc`
229+
}
230+
231+
function getReactNativePackagePath() {
232+
return path.join('node_modules', 'react-native')
233+
}
234+
235+
function getHermesOSBin() {
236+
switch (process.platform) {
237+
case 'win32':
238+
return 'win64-bin'
239+
case 'darwin':
240+
return 'osx-bin'
241+
case 'freebsd':
242+
case 'linux':
243+
case 'sunos':
244+
default:
245+
return 'linux64-bin'
246+
}
247+
}
248+
249+
function getComposeSourceMapsPath() {
250+
// detect if compose-source-maps.js script exists
251+
const composeSourceMapsPath = path.join(getReactNativePackagePath(), 'scripts', 'compose-source-maps.js')
252+
if (fs.existsSync(composeSourceMapsPath)) {
253+
return composeSourceMapsPath
254+
}
255+
return null
256+
}

src/bundle/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
import { bundle } from './bundle'
2-
import { getAppVersion } from './utils'
32

4-
export { bundle, getAppVersion }
3+
export { bundle }

src/bundle/types.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
export type Hashes = Record<string, string>
22

33
export interface VersionSearchParams {
4-
os: string
4+
platform: string
55
version?: string
66
plistFile?: string
77
plistFilePrefix?: string
88
projectFile?: string
99
buildConfigurationName?: string
1010
xcodeTargetName?: string
1111
gradleFile?: string
12+
xcodeProjectFile?: string
1213
}
1314

14-
export interface BundlerConfig {
15+
export interface BundlerConfig extends VersionSearchParams {
1516
os: string
1617
entryFile: string
1718
reinstallNodeModulesCommand: string
@@ -26,13 +27,7 @@ export interface BundlerConfig {
2627
development?: boolean
2728
}
2829

29-
type Config = Partial<BundlerConfig> & VersionSearchParams
30-
31-
export type Os = { os: string }
32-
export type App = { app: string }
33-
export type OsOrApp = Os | App
34-
35-
export type CommandArgs = Omit<Config, 'os'> & OsOrApp
30+
export type CommandArgs = Partial<BundlerConfig> & { os: string }
3631

3732
export type BundleArgs = CommandArgs & {
3833
base: string

src/bundle/utils.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,10 @@ import util from 'util'
33
import * as fs from 'fs'
44
import { tmpdir } from 'os'
55
import * as Path from 'path'
6-
import type { BundlerConfig, CommandArgs, Os, OsOrApp, VersionSearchParams } from './types'
6+
import type { BundlerConfig, CommandArgs } from './types'
77

88
export const ROOT = process.env.PWD ?? ''
99

10-
export const {
11-
getReactNativeProjectAppVersion,
12-
} = require('appcenter-cli/dist/commands/codepush/lib/react-native-utils')
13-
14-
export function getAppVersion(versionSearchParams: VersionSearchParams, projectRoot?: string): Promise<string> {
15-
return getReactNativeProjectAppVersion(versionSearchParams, projectRoot)
16-
}
17-
1810
export function isDirectory(path: string): boolean {
1911
return fs.statSync(path).isDirectory()
2012
}
@@ -72,24 +64,16 @@ export function installNodeModulesCommand() {
7264
return 'npm install'
7365
}
7466

75-
export function getPlatform(app: string): string {
76-
const command = `appcenter apps show --app ${app} --output json`
77-
const json = JSON.parse(child.execSync(command).toString().trim())
78-
79-
return json.os.toLowerCase()
80-
}
81-
82-
const hasOs = (args: OsOrApp): args is Os => 'os' in args && (args.os === 'ios' || args.os === 'android')
83-
8467
export function buildBundleConfig(args: CommandArgs): BundlerConfig {
85-
const os = hasOs(args) ? args.os : getPlatform(args.app)
68+
const { os } = args
8669

8770
return {
8871
outputDir: Path.join(tmpdir(), 'codepush-diff'),
8972
sourcemapOutputDir: Path.join(tmpdir(), args.sourcemapOutput ?? 'codepush-diff-sourcemap'),
9073
bundleName: os === 'ios' ? 'main.jsbundle' : `index.${os}.bundle`,
9174
...args,
9275
os,
76+
platform: os,
9377
entryFile: args.entryFile ?? defaultEntryFile(os),
9478
reinstallNodeModulesCommand: args.reinstallNodeModulesCommand ?? installNodeModulesCommand(),
9579
}

src/cli/bundleArgs.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import type { Argv } from 'yargs'
22

33
export const bundleArgs = <T = {}>(yargs: Argv<T>) =>
44
yargs
5+
.positional('os', {
6+
type: 'string',
7+
demandOption: true,
8+
alias: ['platform'],
9+
})
510
.option('base', { type: 'string', demandOption: true })
611
.option('reinstallNodeModulesCommand', { type: 'string', alias: ['npm'] })
712
.option('rest', { type: 'string', default: '' })
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { type Argv } from 'yargs'
22

3-
export const appcenterArgs = <T = {}>(yargs: Argv<T>) =>
3+
export const codepushArgs = <T = {}>(yargs: Argv<T>) =>
44
yargs
55
.option('deployment-name', { alias: 'd', default: 'Staging', type: 'string' })
6+
.option('cmd', { type: 'string', alias: 'cmd' })
67
.option('description', { type: 'string' })
78
.option('private-key-path', { alias: 'k', type: 'string' })
89
.option('rollout', { alias: 'r', default: '100', type: 'string' })
@@ -32,3 +33,7 @@ export const appcenterArgs = <T = {}>(yargs: Argv<T>) =>
3233
.option('extra-hermes-flag', { alias: ['extra-hermes-flags'], type: 'array', default: [] })
3334
.option('use-hermes', { default: true, type: 'boolean' })
3435
.option('disable-duplicate-release-error', { type: 'boolean', default: true })
36+
.option('buildConfigurationName', {
37+
alias: 'c',
38+
type: 'string',
39+
})

0 commit comments

Comments
 (0)