ubriot 1.0.0 → 1.1.0
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/dist/api.js +6 -3
- package/dist/index.js +40 -1
- package/dist/migrate.js +282 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -7,14 +7,17 @@ exports.makeClient = makeClient;
|
|
|
7
7
|
exports.handleError = handleError;
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
9
|
const config_1 = require("./config");
|
|
10
|
+
function normalizeApiBase(apiBase) {
|
|
11
|
+
return apiBase.replace(/\/+$/, "");
|
|
12
|
+
}
|
|
10
13
|
async function refreshAccessToken() {
|
|
11
14
|
const cfg = (0, config_1.readConfig)();
|
|
12
15
|
if (!cfg.refreshToken)
|
|
13
16
|
return null;
|
|
14
17
|
try {
|
|
15
|
-
const { data } = await axios_1.default.post(`${cfg.apiBase}/auth/refresh`, {
|
|
18
|
+
const { data } = await axios_1.default.post(`${normalizeApiBase(cfg.apiBase)}/auth/refresh`, {
|
|
16
19
|
refreshToken: cfg.refreshToken,
|
|
17
|
-
});
|
|
20
|
+
}, { timeout: 120000 });
|
|
18
21
|
const token = data?.user?.token;
|
|
19
22
|
if (!token)
|
|
20
23
|
return null;
|
|
@@ -28,7 +31,7 @@ async function refreshAccessToken() {
|
|
|
28
31
|
function makeClient() {
|
|
29
32
|
const cfg = (0, config_1.readConfig)();
|
|
30
33
|
const client = axios_1.default.create({
|
|
31
|
-
baseURL: cfg.apiBase,
|
|
34
|
+
baseURL: normalizeApiBase(cfg.apiBase),
|
|
32
35
|
headers: cfg.token ? { Authorization: `Bearer ${cfg.token}` } : {},
|
|
33
36
|
timeout: 120000,
|
|
34
37
|
});
|
package/dist/index.js
CHANGED
|
@@ -38,11 +38,12 @@ const commander_1 = require("commander");
|
|
|
38
38
|
const readline_1 = require("./readline");
|
|
39
39
|
const config_1 = require("./config");
|
|
40
40
|
const api_1 = require("./api");
|
|
41
|
+
const migrate_1 = require("./migrate");
|
|
41
42
|
const child_process_1 = require("child_process");
|
|
42
43
|
const fs_1 = require("fs");
|
|
43
44
|
const path_1 = require("path");
|
|
44
45
|
const crypto_1 = require("crypto");
|
|
45
|
-
commander_1.program.name("ubriot").description("Ubriot, self-hosted Expo release infrastructure").version("
|
|
46
|
+
commander_1.program.name("ubriot").description("Ubriot, self-hosted Expo release infrastructure").version("1.1.0");
|
|
46
47
|
const MIME = {
|
|
47
48
|
".avif": "image/avif",
|
|
48
49
|
".bundle": "application/javascript",
|
|
@@ -1872,4 +1873,42 @@ updates
|
|
|
1872
1873
|
(0, api_1.handleError)(err);
|
|
1873
1874
|
}
|
|
1874
1875
|
});
|
|
1876
|
+
// ─── Migrate ─────────────────────────────────────────────────────────────────
|
|
1877
|
+
const migrate = commander_1.program.command("migrate").description("Migrate onto Ubriot from other services");
|
|
1878
|
+
migrate
|
|
1879
|
+
.command("codepush")
|
|
1880
|
+
.description("Migrate a React Native app from App Center CodePush to Ubriot OTA")
|
|
1881
|
+
.option("--project-dir <dir>", "App directory to scan", ".")
|
|
1882
|
+
.option("--expo-config <path>", "Path to app.json", "app.json")
|
|
1883
|
+
.option("--app <slug>", "Ubriot app slug for the update URL; defaults to expo.slug from app.json")
|
|
1884
|
+
.option("--channel <channel>", "OTA channel", "production")
|
|
1885
|
+
.option("--runtime-version <version>", "Runtime version to write into app.json")
|
|
1886
|
+
.option("--apply", "Apply the changes (default is a dry run)", false)
|
|
1887
|
+
.option("--yes", "Skip the clean-git-tree safety check", false)
|
|
1888
|
+
.action((opts) => {
|
|
1889
|
+
try {
|
|
1890
|
+
const plan = (0, migrate_1.planCodePushMigration)({
|
|
1891
|
+
projectDir: opts.projectDir,
|
|
1892
|
+
expoConfigPath: opts.expoConfig,
|
|
1893
|
+
app: opts.app,
|
|
1894
|
+
});
|
|
1895
|
+
(0, migrate_1.renderPlan)(plan);
|
|
1896
|
+
if (!plan.found)
|
|
1897
|
+
return;
|
|
1898
|
+
if (!opts.apply) {
|
|
1899
|
+
console.log("\nDry run. Re-run with --apply to make these changes.");
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
(0, migrate_1.applyCodePushMigration)({
|
|
1903
|
+
plan,
|
|
1904
|
+
apiBase: (0, config_1.readConfig)().apiBase,
|
|
1905
|
+
channel: opts.channel,
|
|
1906
|
+
runtimeVersion: opts.runtimeVersion,
|
|
1907
|
+
requireCleanGit: !opts.yes,
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
catch (err) {
|
|
1911
|
+
(0, api_1.handleError)(err);
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1875
1914
|
commander_1.program.parse(process.argv);
|
package/dist/migrate.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.planCodePushMigration = planCodePushMigration;
|
|
4
|
+
exports.renderPlan = renderPlan;
|
|
5
|
+
exports.applyCodePushMigration = applyCodePushMigration;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "Pods", ".expo", ".next", "coverage"]);
|
|
10
|
+
const SOURCE_EXT = new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
11
|
+
const CODE_PUSH_PKG = "react-native-code-push";
|
|
12
|
+
function walk(dir, out, depth = 0) {
|
|
13
|
+
if (depth > 8)
|
|
14
|
+
return;
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = (0, fs_1.readdirSync)(dir);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const name of entries) {
|
|
23
|
+
if (SKIP_DIRS.has(name))
|
|
24
|
+
continue;
|
|
25
|
+
const full = (0, path_1.join)(dir, name);
|
|
26
|
+
let st;
|
|
27
|
+
try {
|
|
28
|
+
st = (0, fs_1.statSync)(full);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (st.isDirectory())
|
|
34
|
+
walk(full, out, depth + 1);
|
|
35
|
+
else
|
|
36
|
+
out.push(full);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function readJson(path) {
|
|
40
|
+
return JSON.parse((0, fs_1.readFileSync)(path, "utf8"));
|
|
41
|
+
}
|
|
42
|
+
function expoBlock(json) {
|
|
43
|
+
return json.expo ?? json;
|
|
44
|
+
}
|
|
45
|
+
function planCodePushMigration(input) {
|
|
46
|
+
const projectDir = (0, path_1.resolve)(input.projectDir);
|
|
47
|
+
const appJsonPath = (0, path_1.resolve)(projectDir, input.expoConfigPath);
|
|
48
|
+
const findings = [];
|
|
49
|
+
const warnings = [];
|
|
50
|
+
const sourceFiles = [];
|
|
51
|
+
const iosKeyFiles = [];
|
|
52
|
+
const androidKeyFiles = [];
|
|
53
|
+
let slug = input.app;
|
|
54
|
+
let runtimeVersion;
|
|
55
|
+
if ((0, fs_1.existsSync)(appJsonPath)) {
|
|
56
|
+
try {
|
|
57
|
+
const expo = expoBlock(readJson(appJsonPath));
|
|
58
|
+
slug = slug ?? expo.slug;
|
|
59
|
+
if (typeof expo.runtimeVersion === "string")
|
|
60
|
+
runtimeVersion = expo.runtimeVersion;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
warnings.push(`Could not parse ${(0, path_1.relative)(projectDir, appJsonPath)} as JSON.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
warnings.push(`No app.json found at ${(0, path_1.relative)(projectDir, appJsonPath)}. Ubriot OTA needs an Expo config; create one before applying.`);
|
|
68
|
+
}
|
|
69
|
+
let packageJsonPath;
|
|
70
|
+
const pkgPath = (0, path_1.join)(projectDir, "package.json");
|
|
71
|
+
if ((0, fs_1.existsSync)(pkgPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const pkg = readJson(pkgPath);
|
|
74
|
+
const inDeps = pkg.dependencies?.[CODE_PUSH_PKG];
|
|
75
|
+
const inDev = pkg.devDependencies?.[CODE_PUSH_PKG];
|
|
76
|
+
if (inDeps || inDev) {
|
|
77
|
+
packageJsonPath = pkgPath;
|
|
78
|
+
findings.push({ file: (0, path_1.relative)(projectDir, pkgPath), kind: "package", detail: `${CODE_PUSH_PKG}@${inDeps ?? inDev}` });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
warnings.push("Could not parse package.json.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const all = [];
|
|
86
|
+
walk(projectDir, all);
|
|
87
|
+
for (const file of all) {
|
|
88
|
+
const ext = (0, path_1.extname)(file);
|
|
89
|
+
const name = (0, path_1.basename)(file);
|
|
90
|
+
const rel = (0, path_1.relative)(projectDir, file);
|
|
91
|
+
let content = "";
|
|
92
|
+
try {
|
|
93
|
+
content = (0, fs_1.readFileSync)(file, "utf8");
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (SOURCE_EXT.has(ext)) {
|
|
99
|
+
const hasImport = content.includes(CODE_PUSH_PKG);
|
|
100
|
+
const hasWrapper = /(^|[^.\w])codePush\s*\(/.test(content) || /@codePush\b/.test(content);
|
|
101
|
+
if (hasImport)
|
|
102
|
+
findings.push({ file: rel, kind: "source-import", detail: `imports ${CODE_PUSH_PKG}` });
|
|
103
|
+
if (hasWrapper) {
|
|
104
|
+
findings.push({ file: rel, kind: "source-wrapper", detail: "uses codePush() wrapper or decorator" });
|
|
105
|
+
sourceFiles.push(file);
|
|
106
|
+
}
|
|
107
|
+
else if (hasImport) {
|
|
108
|
+
sourceFiles.push(file);
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (name === "Info.plist" && content.includes("CodePushDeploymentKey")) {
|
|
113
|
+
iosKeyFiles.push(file);
|
|
114
|
+
findings.push({ file: rel, kind: "ios-key", detail: "CodePushDeploymentKey in Info.plist" });
|
|
115
|
+
}
|
|
116
|
+
if (name === "strings.xml" && content.includes("CodePushDeploymentKey")) {
|
|
117
|
+
androidKeyFiles.push(file);
|
|
118
|
+
findings.push({ file: rel, kind: "android-key", detail: "CodePushDeploymentKey in strings.xml" });
|
|
119
|
+
}
|
|
120
|
+
if ((name.startsWith("AppDelegate") || name.startsWith("MainApplication")) && content.includes("CodePush")) {
|
|
121
|
+
findings.push({ file: rel, kind: "native-bundle", detail: `${name} references CodePush for the JS bundle URL` });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
found: findings.length > 0,
|
|
126
|
+
projectDir,
|
|
127
|
+
appJsonPath,
|
|
128
|
+
slug,
|
|
129
|
+
runtimeVersion,
|
|
130
|
+
packageJsonPath,
|
|
131
|
+
sourceFiles: [...new Set(sourceFiles)],
|
|
132
|
+
iosKeyFiles,
|
|
133
|
+
androidKeyFiles,
|
|
134
|
+
findings,
|
|
135
|
+
warnings,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function renderPlan(plan) {
|
|
139
|
+
if (!plan.found) {
|
|
140
|
+
console.log("No react-native-code-push usage found. Nothing to migrate.");
|
|
141
|
+
for (const w of plan.warnings)
|
|
142
|
+
console.log(` note: ${w}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
console.log(`Found ${plan.findings.length} CodePush reference(s) in ${plan.projectDir}:\n`);
|
|
146
|
+
for (const f of plan.findings) {
|
|
147
|
+
console.log(` [${f.kind}] ${f.file} ${f.detail}`);
|
|
148
|
+
}
|
|
149
|
+
console.log("\nPlan on --apply:");
|
|
150
|
+
console.log(` 1. Back up every edited file to <file>.codepush.bak`);
|
|
151
|
+
if (plan.packageJsonPath)
|
|
152
|
+
console.log(` 2. Remove ${CODE_PUSH_PKG} from package.json`);
|
|
153
|
+
console.log(` 3. Write expo.updates (url, channel, runtimeVersion) into ${(0, path_1.relative)(plan.projectDir, plan.appJsonPath)}`);
|
|
154
|
+
if (plan.sourceFiles.length)
|
|
155
|
+
console.log(` 4. Remove CodePush imports and unwrap codePush() in ${plan.sourceFiles.length} source file(s), best-effort`);
|
|
156
|
+
if (plan.iosKeyFiles.length || plan.androidKeyFiles.length)
|
|
157
|
+
console.log(` 5. Strip CodePushDeploymentKey from ${plan.iosKeyFiles.length + plan.androidKeyFiles.length} native file(s)`);
|
|
158
|
+
if (plan.warnings.length) {
|
|
159
|
+
console.log("\nWarnings:");
|
|
160
|
+
for (const w of plan.warnings)
|
|
161
|
+
console.log(` ! ${w}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function backup(file) {
|
|
165
|
+
const bak = `${file}.codepush.bak`;
|
|
166
|
+
if (!(0, fs_1.existsSync)(bak))
|
|
167
|
+
(0, fs_1.copyFileSync)(file, bak);
|
|
168
|
+
}
|
|
169
|
+
function unwrapCodePush(content) {
|
|
170
|
+
let next = content;
|
|
171
|
+
next = next.replace(/^.*(?:from|require\s*\()\s*["']react-native-code-push["'].*$\r?\n?/gm, "");
|
|
172
|
+
next = next.replace(/@codePush\s*\([^)]*\)\s*/g, "");
|
|
173
|
+
next = next.replace(/codePush\s*\([^()]*\)\s*\(\s*([A-Za-z0-9_$]+)\s*\)/g, "$1");
|
|
174
|
+
next = next.replace(/codePush\s*\(\s*([A-Za-z0-9_$]+)\s*\)/g, "$1");
|
|
175
|
+
const residual = /code[Pp]ush\s*[.(]/.test(next) ||
|
|
176
|
+
/\bCodePush\b/.test(next) ||
|
|
177
|
+
next.includes("react-native-code-push");
|
|
178
|
+
return { next, changed: next !== content, residual };
|
|
179
|
+
}
|
|
180
|
+
function applyCodePushMigration(input) {
|
|
181
|
+
const { plan, apiBase, channel } = input;
|
|
182
|
+
if (input.requireCleanGit) {
|
|
183
|
+
const res = (0, child_process_1.spawnSync)("git", ["status", "--porcelain"], { cwd: plan.projectDir, encoding: "utf8" });
|
|
184
|
+
if (res.status === 0 && res.stdout.trim().length > 0) {
|
|
185
|
+
throw new Error("Working tree is not clean. Commit or stash first, or re-run with --yes to override.");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const changed = [];
|
|
189
|
+
const manual = [];
|
|
190
|
+
let sourceChanged = false;
|
|
191
|
+
if (plan.packageJsonPath) {
|
|
192
|
+
backup(plan.packageJsonPath);
|
|
193
|
+
const pkg = readJson(plan.packageJsonPath);
|
|
194
|
+
if (pkg.dependencies)
|
|
195
|
+
delete pkg.dependencies[CODE_PUSH_PKG];
|
|
196
|
+
if (pkg.devDependencies)
|
|
197
|
+
delete pkg.devDependencies[CODE_PUSH_PKG];
|
|
198
|
+
(0, fs_1.writeFileSync)(plan.packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
199
|
+
changed.push((0, path_1.relative)(plan.projectDir, plan.packageJsonPath));
|
|
200
|
+
}
|
|
201
|
+
if ((0, fs_1.existsSync)(plan.appJsonPath)) {
|
|
202
|
+
backup(plan.appJsonPath);
|
|
203
|
+
const json = readJson(plan.appJsonPath);
|
|
204
|
+
const hasExpoKey = Boolean(json.expo);
|
|
205
|
+
const expo = hasExpoKey ? json.expo : json;
|
|
206
|
+
const slug = plan.slug ?? expo.slug;
|
|
207
|
+
if (!slug)
|
|
208
|
+
throw new Error("No app slug. Pass --app <slug> or set expo.slug in app.json.");
|
|
209
|
+
expo.updates = {
|
|
210
|
+
...(expo.updates ?? {}),
|
|
211
|
+
enabled: true,
|
|
212
|
+
url: `${apiBase.replace(/\/$/, "")}/ubriot/updates/manifest?appId=${encodeURIComponent(slug)}`,
|
|
213
|
+
requestHeaders: { ...(expo.updates?.requestHeaders ?? {}), "expo-channel-name": channel },
|
|
214
|
+
};
|
|
215
|
+
const rv = input.runtimeVersion ?? plan.runtimeVersion;
|
|
216
|
+
if (rv)
|
|
217
|
+
expo.runtimeVersion = rv;
|
|
218
|
+
else if (!expo.runtimeVersion)
|
|
219
|
+
manual.push("Set expo.runtimeVersion in app.json (pass --runtime-version to do it automatically).");
|
|
220
|
+
if (hasExpoKey)
|
|
221
|
+
json.expo = expo;
|
|
222
|
+
(0, fs_1.writeFileSync)(plan.appJsonPath, `${JSON.stringify(json, null, 2)}\n`);
|
|
223
|
+
changed.push((0, path_1.relative)(plan.projectDir, plan.appJsonPath));
|
|
224
|
+
}
|
|
225
|
+
for (const file of plan.sourceFiles) {
|
|
226
|
+
let content;
|
|
227
|
+
try {
|
|
228
|
+
content = (0, fs_1.readFileSync)(file, "utf8");
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const { next, changed: didChange, residual } = unwrapCodePush(content);
|
|
234
|
+
if (didChange) {
|
|
235
|
+
backup(file);
|
|
236
|
+
(0, fs_1.writeFileSync)(file, next);
|
|
237
|
+
changed.push((0, path_1.relative)(plan.projectDir, file));
|
|
238
|
+
sourceChanged = true;
|
|
239
|
+
}
|
|
240
|
+
if (residual)
|
|
241
|
+
manual.push(`Review ${(0, path_1.relative)(plan.projectDir, file)}: a CodePush reference could not be rewritten automatically.`);
|
|
242
|
+
}
|
|
243
|
+
if (sourceChanged) {
|
|
244
|
+
manual.push("Verify the app still compiles. If you used the imperative CodePush API (CodePush.sync / checkForUpdate), migrate those calls to expo-updates (Updates.checkForUpdateAsync / fetchUpdateAsync / reloadAsync) by hand.");
|
|
245
|
+
}
|
|
246
|
+
for (const file of [...plan.iosKeyFiles, ...plan.androidKeyFiles]) {
|
|
247
|
+
let content;
|
|
248
|
+
try {
|
|
249
|
+
content = (0, fs_1.readFileSync)(file, "utf8");
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
let next = content;
|
|
255
|
+
next = next.replace(/^\s*<key>CodePushDeploymentKey<\/key>\s*\n\s*<string>[^<]*<\/string>\s*\n/gm, "");
|
|
256
|
+
next = next.replace(/^\s*<string\b[^>]*\bname="CodePushDeploymentKey"[^>]*>[^<]*<\/string>\s*\n/gm, "");
|
|
257
|
+
if (next !== content) {
|
|
258
|
+
backup(file);
|
|
259
|
+
(0, fs_1.writeFileSync)(file, next);
|
|
260
|
+
changed.push((0, path_1.relative)(plan.projectDir, file));
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
manual.push(`Remove the CodePushDeploymentKey entry from ${(0, path_1.relative)(plan.projectDir, file)} by hand.`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
console.log(`\nApplied. Edited ${changed.length} file(s):`);
|
|
267
|
+
for (const c of changed)
|
|
268
|
+
console.log(` ~ ${c}`);
|
|
269
|
+
console.log("\nFinish the migration with:");
|
|
270
|
+
console.log(" 1. npx expo install expo-updates");
|
|
271
|
+
if (!plan.slug)
|
|
272
|
+
console.log(" 2. Create or link a Ubriot app, then set --app <slug>");
|
|
273
|
+
else
|
|
274
|
+
console.log(` 2. ubriot apps create --name "<App Name>" --slug ${plan.slug} (skip if it already exists)`);
|
|
275
|
+
console.log(` 3. ubriot update --app ${plan.slug ?? "<slug>"} --channel ${channel}`);
|
|
276
|
+
if (manual.length) {
|
|
277
|
+
console.log("\nManual steps still required:");
|
|
278
|
+
for (const m of manual)
|
|
279
|
+
console.log(` ! ${m}`);
|
|
280
|
+
}
|
|
281
|
+
console.log("\nBackups written as <file>.codepush.bak next to each edited file.");
|
|
282
|
+
}
|