@bipopush/cli 0.3.0-beta.2 → 0.3.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/diff.wasm +0 -0
- package/dist/index.js +1 -224
- package/package.json +3 -5
- package/dist/commands.js +0 -363
- package/dist/compat.js +0 -96
- package/dist/config.js +0 -29
- package/dist/http.js +0 -41
- package/dist/keys.js +0 -41
- package/dist/login.js +0 -73
- package/dist/migrate.js +0 -65
- package/dist/pack.js +0 -174
- package/dist/project.js +0 -191
package/dist/migrate.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
/** OTA libraries BipoPush replaces, by package name. */
|
|
4
|
-
const SOURCES = {
|
|
5
|
-
"react-native-code-push": "CodePush",
|
|
6
|
-
"@revopush/react-native-code-push": "RevoPush",
|
|
7
|
-
"@code-push-next/react-native-code-push": "CodePush (code-push-next)",
|
|
8
|
-
"expo-updates": "Expo Updates",
|
|
9
|
-
};
|
|
10
|
-
/** Files under src-like folders that import the old library (at most 20, for the summary). */
|
|
11
|
-
function importers(cwd, names) {
|
|
12
|
-
const found = [];
|
|
13
|
-
const walk = (dir) => {
|
|
14
|
-
for (const entry of readdirSync(dir)) {
|
|
15
|
-
if (found.length >= 20 || entry === "node_modules" || entry.startsWith(".") || entry === "ios" || entry === "android")
|
|
16
|
-
continue;
|
|
17
|
-
const full = path.join(dir, entry);
|
|
18
|
-
if (statSync(full).isDirectory())
|
|
19
|
-
walk(full);
|
|
20
|
-
else if (/\.(jsx?|tsx?)$/.test(entry) && names.some((n) => readFileSync(full, "utf8").includes(`"${n}"`) || readFileSync(full, "utf8").includes(`'${n}'`))) {
|
|
21
|
-
found.push(path.relative(cwd, full));
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
walk(cwd);
|
|
26
|
-
return found;
|
|
27
|
-
}
|
|
28
|
-
/** Prints what to change to move this project from CodePush, RevoPush or expo-updates to BipoPush. */
|
|
29
|
-
export function migrate(cwd) {
|
|
30
|
-
const pkgFile = path.join(cwd, "package.json");
|
|
31
|
-
if (!existsSync(pkgFile))
|
|
32
|
-
throw new Error("Run this in your React Native / Expo project folder (no package.json here)");
|
|
33
|
-
const pkg = JSON.parse(readFileSync(pkgFile, "utf8"));
|
|
34
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
35
|
-
const present = Object.keys(SOURCES).filter((name) => deps[name]);
|
|
36
|
-
if (present.length === 0) {
|
|
37
|
-
console.log("No CodePush, RevoPush or expo-updates in this project. Start with: npx @bipopush/cli init");
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const codePush = present.filter((n) => n !== "expo-updates");
|
|
41
|
-
const expo = present.includes("expo-updates");
|
|
42
|
-
console.log(`Found ${present.map((n) => SOURCES[n]).join(" and ")}. To move to BipoPush:\n`);
|
|
43
|
-
let step = 1;
|
|
44
|
-
const say = (text) => console.log(`${step++}. ${text}`);
|
|
45
|
-
say(`Install the SDK: npm install @bipopush/react-native${expo ? " (Expo: npx expo install @bipopush/react-native)" : ""}`);
|
|
46
|
-
say(`Remove ${present.join(", ")} (npm uninstall), and its native setup:`);
|
|
47
|
-
if (codePush.length) {
|
|
48
|
-
console.log(" iOS: CodePush.bundleURL() in AppDelegate and CodePushDeploymentKey in Info.plist");
|
|
49
|
-
console.log(" Android: CodePush in MainApplication and CodePushDeploymentKey in strings.xml");
|
|
50
|
-
}
|
|
51
|
-
if (expo)
|
|
52
|
-
console.log(' app.json: the "updates" block and "runtimeVersion" (expo-updates); keep your other plugins');
|
|
53
|
-
say("Register the app and write its keys into the native project: npx @bipopush/cli init");
|
|
54
|
-
const files = importers(cwd, present);
|
|
55
|
-
const subpath = codePush.length ? "codepush" : "expo-updates";
|
|
56
|
-
say(`Keep your code: change the import to "@bipopush/react-native/${subpath}"${files.length ? ` (${files.length} file${files.length === 1 ? "" : "s"}: ${files.slice(0, 5).join(", ")}${files.length > 5 ? "…" : ""})` : ""},`);
|
|
57
|
-
console.log(` or leave imports alone and in metro.config.js: module.exports = require("@bipopush/react-native/metro").withBipoPush(config, { ${codePush.length ? "codePush: true" : "expoUpdates: true"} })`);
|
|
58
|
-
say("Rebuild the native app once (it now embeds BipoPush), then ship updates: npx @bipopush/cli release");
|
|
59
|
-
if (codePush.length) {
|
|
60
|
-
console.log("\nYour CodePush scripts keep working: bipopush release-react <app> <platform> -d Production, promote, rollback and patch.");
|
|
61
|
-
console.log("Deployments become channels: Production → production, Staging → staging.");
|
|
62
|
-
}
|
|
63
|
-
if (expo)
|
|
64
|
-
console.log("\nYour EAS commands map too: bipopush update --channel production --message \"…\" --rollout-percentage 10.");
|
|
65
|
-
}
|
package/dist/pack.js
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { brotliCompressSync, brotliDecompressSync, constants } from "node:zlib";
|
|
8
|
-
import { signDigest } from "./keys.js";
|
|
9
|
-
/** A patch is only worth shipping when it is clearly smaller than the compressed full bundle. */
|
|
10
|
-
export const PATCH_RATIO = 0.6;
|
|
11
|
-
const localRequire = createRequire(import.meta.url);
|
|
12
|
-
function binFrom(root, request) {
|
|
13
|
-
return createRequire(path.join(root, "package.json")).resolve(request);
|
|
14
|
-
}
|
|
15
|
-
export function findHermesc(root) {
|
|
16
|
-
const os = process.platform === "darwin" ? "osx-bin" : process.platform === "win32" ? "win64-bin" : "linux64-bin";
|
|
17
|
-
const exe = process.platform === "win32" ? "hermesc.exe" : "hermesc";
|
|
18
|
-
const candidates = [];
|
|
19
|
-
for (const pkg of ["hermes-compiler/package.json", "react-native/package.json"]) {
|
|
20
|
-
try {
|
|
21
|
-
const dir = path.dirname(binFrom(root, pkg));
|
|
22
|
-
candidates.push(path.join(dir, "hermesc", os, exe), path.join(dir, "sdks", "hermesc", os, exe));
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
// package not installed
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
const found = candidates.find(existsSync);
|
|
29
|
-
if (!found)
|
|
30
|
-
throw new Error("hermesc not found — install the app dependencies first (npm install)");
|
|
31
|
-
return found;
|
|
32
|
-
}
|
|
33
|
-
export function sha256Hex(data) {
|
|
34
|
-
return createHash("sha256").update(data).digest("hex");
|
|
35
|
-
}
|
|
36
|
-
export function compress(data) {
|
|
37
|
-
return brotliCompressSync(data, {
|
|
38
|
-
params: {
|
|
39
|
-
[constants.BROTLI_PARAM_QUALITY]: 11,
|
|
40
|
-
[constants.BROTLI_PARAM_LGWIN]: 24,
|
|
41
|
-
[constants.BROTLI_PARAM_SIZE_HINT]: data.length,
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
export function decompress(data) {
|
|
46
|
-
return brotliDecompressSync(data);
|
|
47
|
-
}
|
|
48
|
-
/** BSDIFF40 patch (old → new), or null when the optional native addon is unavailable. */
|
|
49
|
-
export async function makePatch(oldData, newData, workDir) {
|
|
50
|
-
let bsdiff;
|
|
51
|
-
try {
|
|
52
|
-
bsdiff = localRequire("bsdiff-nodejs");
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
const oldFile = path.join(workDir, "base.hbc");
|
|
58
|
-
const newFile = path.join(workDir, "new.hbc");
|
|
59
|
-
const patchFile = path.join(workDir, "bundle.bspatch");
|
|
60
|
-
writeFileSync(oldFile, oldData);
|
|
61
|
-
writeFileSync(newFile, newData);
|
|
62
|
-
await bsdiff.diff(oldFile, newFile, patchFile);
|
|
63
|
-
return readFileSync(patchFile);
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Canonical manifest digest (same in the API and the native SDK):
|
|
67
|
-
* "bipopush-manifest-v1\n" + "bundle <sha256>\n" + one "<sha256> <path>\n" per asset, sorted by path.
|
|
68
|
-
*/
|
|
69
|
-
export function manifestDigestHex(bundleSha256, assets) {
|
|
70
|
-
const lines = [...assets].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)).map((a) => `${a.sha256} ${a.path}\n`);
|
|
71
|
-
return sha256Hex(Buffer.from(`bipopush-manifest-v1\nbundle ${bundleSha256}\n${lines.join("")}`));
|
|
72
|
-
}
|
|
73
|
-
/** Walks Metro's assets-dest output; paths are relative to it, which is the folder next to the bundle. */
|
|
74
|
-
export function collectAssets(root) {
|
|
75
|
-
const assets = [];
|
|
76
|
-
const files = new Map();
|
|
77
|
-
const walk = (dir) => {
|
|
78
|
-
for (const name of readdirSync(dir)) {
|
|
79
|
-
if (name === ".DS_Store")
|
|
80
|
-
continue;
|
|
81
|
-
const full = path.join(dir, name);
|
|
82
|
-
if (statSync(full).isDirectory())
|
|
83
|
-
walk(full);
|
|
84
|
-
else {
|
|
85
|
-
const data = readFileSync(full);
|
|
86
|
-
const hash = sha256Hex(data);
|
|
87
|
-
assets.push({ path: path.relative(root, full).split(path.sep).join("/"), sha256: hash, size: data.length });
|
|
88
|
-
files.set(hash, data);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
if (existsSync(root))
|
|
93
|
-
walk(root);
|
|
94
|
-
assets.sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
95
|
-
return { assets, files };
|
|
96
|
-
}
|
|
97
|
-
export function assetsDir(outDir) {
|
|
98
|
-
return path.join(outDir, "assets-out");
|
|
99
|
-
}
|
|
100
|
-
/** Produces the JavaScript bundle with the project's own toolchain (Metro or Expo). */
|
|
101
|
-
export function bundleJs(project, platform, entryFile, outDir) {
|
|
102
|
-
const out = path.join(outDir, `index.${platform}.bundle`);
|
|
103
|
-
const assets = assetsDir(outDir);
|
|
104
|
-
mkdirSync(assets, { recursive: true });
|
|
105
|
-
if (project.type === "expo") {
|
|
106
|
-
const expoCli = binFrom(project.root, "expo/bin/cli");
|
|
107
|
-
execFileSync(process.execPath, [expoCli, "export:embed", "--platform", platform, "--dev", "false", "--minify", "false", "--entry-file", entryFile, "--bundle-output", out, "--assets-dest", assets], { cwd: project.root, stdio: "inherit" });
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
const rnCli = binFrom(project.root, "react-native/cli.js");
|
|
111
|
-
execFileSync(process.execPath, [rnCli, "bundle", "--platform", platform, "--dev", "false", "--minify", "false", "--entry-file", entryFile, "--bundle-output", out, "--assets-dest", assets], { cwd: project.root, stdio: "inherit" });
|
|
112
|
-
}
|
|
113
|
-
return out;
|
|
114
|
-
}
|
|
115
|
-
/** Compiles to Hermes bytecode; with a base, hermesc lays out the output to diff well against it. */
|
|
116
|
-
export function compileHermes(hermesc, jsFile, outDir, baseFile) {
|
|
117
|
-
const out = path.join(outDir, "index.hbc");
|
|
118
|
-
const args = ["-O", "-w", "-emit-binary", `-out=${out}`];
|
|
119
|
-
if (baseFile)
|
|
120
|
-
args.push(`-base-bytecode=${baseFile}`);
|
|
121
|
-
args.push(jsFile);
|
|
122
|
-
execFileSync(hermesc, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
123
|
-
return out;
|
|
124
|
-
}
|
|
125
|
-
export async function packPlatform(opts) {
|
|
126
|
-
const work = mkdtempSync(path.join(tmpdir(), `bipopush-${opts.platform}-`));
|
|
127
|
-
try {
|
|
128
|
-
return await packIn(work, opts);
|
|
129
|
-
}
|
|
130
|
-
finally {
|
|
131
|
-
rmSync(work, { recursive: true, force: true });
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
async function packIn(work, opts) {
|
|
135
|
-
const jsFile = opts.jsFile ?? bundleJs(opts.project, opts.platform, opts.entryFile, work);
|
|
136
|
-
const hermesc = findHermesc(opts.project.root);
|
|
137
|
-
let baseFile = null;
|
|
138
|
-
if (opts.base) {
|
|
139
|
-
baseFile = path.join(work, "base-input.hbc");
|
|
140
|
-
writeFileSync(baseFile, opts.base.hbc);
|
|
141
|
-
}
|
|
142
|
-
const hbc = readFileSync(compileHermes(hermesc, jsFile, work, baseFile));
|
|
143
|
-
const sha256 = sha256Hex(hbc);
|
|
144
|
-
const full = compress(hbc);
|
|
145
|
-
const { assets, files } = collectAssets(opts.assetsRoot ?? assetsDir(work));
|
|
146
|
-
// With assets, the signature covers the whole manifest so no file can be swapped.
|
|
147
|
-
const signedDigest = assets.length > 0 ? manifestDigestHex(sha256, assets) : sha256;
|
|
148
|
-
let patch = null;
|
|
149
|
-
let patchNote = "no previous release for this channel and version";
|
|
150
|
-
if (opts.base) {
|
|
151
|
-
const data = await makePatch(opts.base.hbc, hbc, work);
|
|
152
|
-
if (!data)
|
|
153
|
-
patchNote = "bsdiff-nodejs not installed";
|
|
154
|
-
else if (data.length >= full.length * PATCH_RATIO)
|
|
155
|
-
patchNote = `patch ${data.length} B is not smaller than ${PATCH_RATIO * 100}% of full`;
|
|
156
|
-
else {
|
|
157
|
-
patch = { baseArtifactId: opts.base.artifactId, data };
|
|
158
|
-
patchNote = "";
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return {
|
|
162
|
-
platform: opts.platform,
|
|
163
|
-
appVersion: opts.appVersion,
|
|
164
|
-
jsSize: readFileSync(jsFile).length,
|
|
165
|
-
bundleSize: hbc.length,
|
|
166
|
-
sha256,
|
|
167
|
-
signature: signDigest(opts.privateKey, signedDigest),
|
|
168
|
-
full,
|
|
169
|
-
patch,
|
|
170
|
-
patchNote,
|
|
171
|
-
assets,
|
|
172
|
-
assetFiles: files,
|
|
173
|
-
};
|
|
174
|
-
}
|
package/dist/project.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
export const PROJECT_FILE = "bipopush.json";
|
|
5
|
-
function readJson(file) {
|
|
6
|
-
try {
|
|
7
|
-
return JSON.parse(readFileSync(file, "utf8"));
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
export function loadProjectConfig(root) {
|
|
14
|
-
return readJson(path.join(root, PROJECT_FILE));
|
|
15
|
-
}
|
|
16
|
-
export function saveProjectConfig(root, config) {
|
|
17
|
-
writeFileSync(path.join(root, PROJECT_FILE), `${JSON.stringify(config, null, 2)}\n`);
|
|
18
|
-
}
|
|
19
|
-
function pbxSetting(pbx, key) {
|
|
20
|
-
const values = [...pbx.matchAll(new RegExp(`${key} = "?([^";]+)"?;`, "g"))].map((m) => m[1].trim());
|
|
21
|
-
return values.find((v) => !v.includes("$(")) ?? values[0] ?? null;
|
|
22
|
-
}
|
|
23
|
-
function plistValue(file, keyPath) {
|
|
24
|
-
try {
|
|
25
|
-
return execFileSync("plutil", ["-extract", keyPath, "raw", "-o", "-", file], { stdio: ["ignore", "pipe", "ignore"] })
|
|
26
|
-
.toString()
|
|
27
|
-
.trim();
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function readIos(root) {
|
|
34
|
-
const iosDir = path.join(root, "ios");
|
|
35
|
-
const empty = { info: { id: null, version: null, build: null, scheme: null }, plist: null };
|
|
36
|
-
if (!existsSync(iosDir))
|
|
37
|
-
return empty;
|
|
38
|
-
const proj = readdirSync(iosDir).find((f) => f.endsWith(".xcodeproj"));
|
|
39
|
-
if (!proj)
|
|
40
|
-
return empty;
|
|
41
|
-
const pbx = readFileSync(path.join(iosDir, proj, "project.pbxproj"), "utf8");
|
|
42
|
-
const productName = proj.replace(/\.xcodeproj$/, "");
|
|
43
|
-
let id = pbxSetting(pbx, "PRODUCT_BUNDLE_IDENTIFIER");
|
|
44
|
-
if (id?.includes("$(PRODUCT_NAME"))
|
|
45
|
-
id = id.replace(/\$\(PRODUCT_NAME[^)]*\)/, productName.replace(/[^A-Za-z0-9-]/g, "-"));
|
|
46
|
-
const plistRel = pbxSetting(pbx, "INFOPLIST_FILE");
|
|
47
|
-
const plist = plistRel ? path.join(iosDir, plistRel.replace("$(SRCROOT)/", "")) : null;
|
|
48
|
-
const scheme = plist ? plistValue(plist, "CFBundleURLTypes.0.CFBundleURLSchemes.0") : null;
|
|
49
|
-
// The version the app reports at runtime is CFBundleShortVersionString: a literal in Expo projects,
|
|
50
|
-
// "$(MARKETING_VERSION)" in bare React Native projects.
|
|
51
|
-
const plistVersion = plist ? plistValue(plist, "CFBundleShortVersionString") : null;
|
|
52
|
-
const version = plistVersion && !plistVersion.includes("$(") ? plistVersion : pbxSetting(pbx, "MARKETING_VERSION");
|
|
53
|
-
// Same for the build: CFBundleVersion, "$(CURRENT_PROJECT_VERSION)" in bare projects.
|
|
54
|
-
const plistBuild = plist ? plistValue(plist, "CFBundleVersion") : null;
|
|
55
|
-
const build = plistBuild && !plistBuild.includes("$(") ? plistBuild : pbxSetting(pbx, "CURRENT_PROJECT_VERSION");
|
|
56
|
-
return { info: { id, version, build, scheme }, plist: plist && existsSync(plist) ? plist : null };
|
|
57
|
-
}
|
|
58
|
-
function androidManifestFile(root) {
|
|
59
|
-
const file = path.join(root, "android", "app", "src", "main", "AndroidManifest.xml");
|
|
60
|
-
return existsSync(file) ? file : null;
|
|
61
|
-
}
|
|
62
|
-
function readAndroid(root) {
|
|
63
|
-
const gradleFile = ["build.gradle", "build.gradle.kts"].map((f) => path.join(root, "android", "app", f)).find(existsSync);
|
|
64
|
-
if (!gradleFile)
|
|
65
|
-
return { id: null, version: null, build: null, scheme: null };
|
|
66
|
-
const gradle = readFileSync(gradleFile, "utf8");
|
|
67
|
-
const id = gradle.match(/applicationId\s*=?\s*["']([^"']+)["']/)?.[1] ?? null;
|
|
68
|
-
const version = gradle.match(/versionName\s*=?\s*["']([^"']+)["']/)?.[1] ?? null;
|
|
69
|
-
const build = gradle.match(/versionCode\s*=?\s*(\d+)/)?.[1] ?? null;
|
|
70
|
-
const manifestFile = path.join(root, "android", "app", "src", "main", "AndroidManifest.xml");
|
|
71
|
-
const manifest = existsSync(manifestFile) ? readFileSync(manifestFile, "utf8") : "";
|
|
72
|
-
const scheme = manifest.match(/<data[^>]*android:scheme="([^"]+)"/)?.[1] ?? null;
|
|
73
|
-
return { id, version, build, scheme };
|
|
74
|
-
}
|
|
75
|
-
/** Detects Expo vs bare React Native and reads native identifiers, versions and URL scheme. */
|
|
76
|
-
export function detectProject(root) {
|
|
77
|
-
const pkg = readJson(path.join(root, "package.json"));
|
|
78
|
-
if (!pkg)
|
|
79
|
-
throw new Error(`No package.json in ${root}`);
|
|
80
|
-
if (!pkg.dependencies?.["react-native"])
|
|
81
|
-
throw new Error("This is not a React Native project (react-native is not a dependency)");
|
|
82
|
-
const appJson = readJson(path.join(root, "app.json"));
|
|
83
|
-
const isExpo = Boolean(pkg.dependencies?.expo && appJson?.expo);
|
|
84
|
-
const ios = readIos(root);
|
|
85
|
-
const android = readAndroid(root);
|
|
86
|
-
const entryFile = pkg.main && existsSync(path.join(root, pkg.main)) ? pkg.main : "index.js";
|
|
87
|
-
if (isExpo && !ios.info.id) {
|
|
88
|
-
const expo = appJson.expo;
|
|
89
|
-
const scheme = Array.isArray(expo.scheme) ? expo.scheme[0] : (expo.scheme ?? null);
|
|
90
|
-
return {
|
|
91
|
-
root,
|
|
92
|
-
type: "expo",
|
|
93
|
-
name: expo.name ?? pkg.name ?? "app",
|
|
94
|
-
entryFile,
|
|
95
|
-
ios: {
|
|
96
|
-
id: expo.ios?.bundleIdentifier ?? null,
|
|
97
|
-
version: expo.ios?.version ?? expo.version ?? null,
|
|
98
|
-
build: expo.ios?.buildNumber != null ? String(expo.ios.buildNumber) : null,
|
|
99
|
-
scheme,
|
|
100
|
-
},
|
|
101
|
-
android: {
|
|
102
|
-
id: expo.android?.package ?? null,
|
|
103
|
-
version: expo.version ?? null,
|
|
104
|
-
build: expo.android?.versionCode != null ? String(expo.android.versionCode) : null,
|
|
105
|
-
scheme,
|
|
106
|
-
},
|
|
107
|
-
iosInfoPlist: null,
|
|
108
|
-
androidManifest: null,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
root,
|
|
113
|
-
type: isExpo ? "expo" : "react-native",
|
|
114
|
-
name: appJson?.expo?.name ?? pkg.name ?? "app",
|
|
115
|
-
entryFile,
|
|
116
|
-
ios: ios.info,
|
|
117
|
-
android,
|
|
118
|
-
iosInfoPlist: ios.plist,
|
|
119
|
-
androidManifest: isExpo ? null : androidManifestFile(root),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Writes BipoPush keys as <meta-data> of <application> in AndroidManifest.xml (the Android module reads
|
|
124
|
-
* them like iOS reads Info.plist). Keys passed as null are removed.
|
|
125
|
-
*/
|
|
126
|
-
export function writeAndroidManifest(file, values) {
|
|
127
|
-
let xml = readFileSync(file, "utf8");
|
|
128
|
-
for (const [key, value] of Object.entries(values)) {
|
|
129
|
-
const existing = new RegExp(`\\s*<meta-data\\s+android:name="${key}"[^>]*/>`, "g");
|
|
130
|
-
xml = xml.replace(existing, "");
|
|
131
|
-
if (value === null)
|
|
132
|
-
continue;
|
|
133
|
-
xml = xml.replace(/(<application\b[^>]*>)/, (tag) => `${tag}\n <meta-data android:name="${key}" android:value="${value}" />`);
|
|
134
|
-
}
|
|
135
|
-
writeFileSync(file, xml);
|
|
136
|
-
}
|
|
137
|
-
/** Adds a VIEW intent-filter for `<scheme>://` deeplinks to the launcher activity. */
|
|
138
|
-
export function addAndroidUrlScheme(file, scheme) {
|
|
139
|
-
const xml = readFileSync(file, "utf8");
|
|
140
|
-
const filter = `
|
|
141
|
-
<intent-filter>
|
|
142
|
-
<action android:name="android.intent.action.VIEW" />
|
|
143
|
-
<category android:name="android.intent.category.DEFAULT" />
|
|
144
|
-
<category android:name="android.intent.category.BROWSABLE" />
|
|
145
|
-
<data android:scheme="${scheme}" />
|
|
146
|
-
</intent-filter>`;
|
|
147
|
-
const launcher = /(<category android:name="android\.intent\.category\.LAUNCHER"\s*\/>\s*<\/intent-filter>)/;
|
|
148
|
-
if (!launcher.test(xml))
|
|
149
|
-
throw new Error(`Could not find the launcher activity in ${file}; add the ${scheme}:// intent-filter by hand.`);
|
|
150
|
-
writeFileSync(file, xml.replace(launcher, (launcherFilter) => `${launcherFilter}${filter}`));
|
|
151
|
-
}
|
|
152
|
-
/** Writes BipoPush keys into the iOS Info.plist so the native module needs no JS configuration. Keys passed as null are removed. */
|
|
153
|
-
export function writeIosInfoPlist(plist, values) {
|
|
154
|
-
for (const [key, value] of Object.entries(values)) {
|
|
155
|
-
if (value !== null)
|
|
156
|
-
execFileSync("plutil", ["-replace", key, "-string", value, plist]);
|
|
157
|
-
else if (plistValue(plist, key) !== null)
|
|
158
|
-
execFileSync("plutil", ["-remove", key, plist]);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
/** Registers a URL scheme (CFBundleURLTypes) so `<scheme>://` deeplinks open the app. */
|
|
162
|
-
export function addIosUrlScheme(plist, scheme) {
|
|
163
|
-
const run = (cmd) => execFileSync("/usr/libexec/PlistBuddy", ["-c", cmd, plist], { stdio: ["ignore", "ignore", "ignore"] });
|
|
164
|
-
try {
|
|
165
|
-
run("Print :CFBundleURLTypes");
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
run("Add :CFBundleURLTypes array");
|
|
169
|
-
}
|
|
170
|
-
run("Add :CFBundleURLTypes:0 dict");
|
|
171
|
-
run("Add :CFBundleURLTypes:0:CFBundleURLSchemes array");
|
|
172
|
-
run(`Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string ${scheme}`);
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Expo (continuous native generation): configure the BipoPush config plugin in app.json instead of
|
|
176
|
-
* editing generated native files. Also sets the URL scheme when one is given and none exists.
|
|
177
|
-
*/
|
|
178
|
-
export function writeExpoConfig(root, plugin, scheme) {
|
|
179
|
-
const file = path.join(root, "app.json");
|
|
180
|
-
const json = readJson(file);
|
|
181
|
-
if (!json?.expo)
|
|
182
|
-
throw new Error("app.json has no expo section");
|
|
183
|
-
const expo = json.expo;
|
|
184
|
-
const plugins = Array.isArray(expo.plugins) ? expo.plugins : [];
|
|
185
|
-
const name = "@bipopush/react-native";
|
|
186
|
-
const rest = plugins.filter((p) => (Array.isArray(p) ? p[0] : p) !== name);
|
|
187
|
-
expo.plugins = [...rest, [name, plugin]];
|
|
188
|
-
if (scheme && !expo.scheme)
|
|
189
|
-
expo.scheme = scheme;
|
|
190
|
-
writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`);
|
|
191
|
-
}
|