hot-updater 0.30.12 → 0.31.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/index.mjs +365 -22
- package/package.json +14 -14
package/dist/index.mjs
CHANGED
|
@@ -25,8 +25,11 @@ import { finished } from "node:stream/promises";
|
|
|
25
25
|
import { Duplex, PassThrough, Readable, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
|
|
26
26
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
27
27
|
import { SourceSkips, createFingerprintAsync, diffFingerprintChangesAsync } from "@expo/fingerprint";
|
|
28
|
+
import { brotliCompress, constants as constants$1 } from "zlib";
|
|
29
|
+
import { assertNodeStoragePlugin } from "@hot-updater/plugin-core";
|
|
30
|
+
import { createBundleDiff } from "@hot-updater/server";
|
|
28
31
|
import net from "node:net";
|
|
29
|
-
import fs$3, { constants as constants$
|
|
32
|
+
import fs$3, { constants as constants$2 } from "node:fs/promises";
|
|
30
33
|
import crypto$1 from "node:crypto";
|
|
31
34
|
import { Kysely, MysqlDialect, PostgresDialect, SqliteDialect } from "kysely";
|
|
32
35
|
import { format } from "sql-formatter";
|
|
@@ -47497,7 +47500,7 @@ const getWslDrivesMountPoint = (() => {
|
|
|
47497
47500
|
const configFilePath = "/etc/wsl.conf";
|
|
47498
47501
|
let isConfigFileExists = false;
|
|
47499
47502
|
try {
|
|
47500
|
-
await fs$3.access(configFilePath, constants$
|
|
47503
|
+
await fs$3.access(configFilePath, constants$2.F_OK);
|
|
47501
47504
|
isConfigFileExists = true;
|
|
47502
47505
|
} catch {}
|
|
47503
47506
|
if (!isConfigFileExists) return defaultMountPoint;
|
|
@@ -47599,7 +47602,7 @@ const baseOpen = async (options) => {
|
|
|
47599
47602
|
const isBundled = !__dirname || __dirname === "/";
|
|
47600
47603
|
let exeLocalXdgOpen = false;
|
|
47601
47604
|
try {
|
|
47602
|
-
await fs$3.access(localXdgOpenPath, constants$
|
|
47605
|
+
await fs$3.access(localXdgOpenPath, constants$2.X_OK);
|
|
47603
47606
|
exeLocalXdgOpen = true;
|
|
47604
47607
|
} catch {}
|
|
47605
47608
|
command = process$1.versions.electron ?? (platform$1 === "android" || isBundled || !exeLocalXdgOpen) ? "xdg-open" : localXdgOpenPath;
|
|
@@ -47759,15 +47762,23 @@ const getFileHashFromFile = async (filepath) => {
|
|
|
47759
47762
|
};
|
|
47760
47763
|
//#endregion
|
|
47761
47764
|
//#region src/utils/bundleManifest.ts
|
|
47762
|
-
const createBundleManifest = async ({ bundleId, targetFiles }) => {
|
|
47765
|
+
const createBundleManifest = async ({ bundleId, signFileHash, targetFiles }) => {
|
|
47763
47766
|
return {
|
|
47764
47767
|
bundleId,
|
|
47765
|
-
assets: Object.fromEntries(await Promise.all([...targetFiles].sort((left, right) => left.name.localeCompare(right.name)).map(async (target) =>
|
|
47768
|
+
assets: Object.fromEntries(await Promise.all([...targetFiles].sort((left, right) => left.name.localeCompare(right.name)).map(async (target) => {
|
|
47769
|
+
const fileHash = await getFileHashFromFile(target.path);
|
|
47770
|
+
const signature = signFileHash ? await signFileHash(fileHash) : void 0;
|
|
47771
|
+
return [target.name, {
|
|
47772
|
+
fileHash,
|
|
47773
|
+
...signature ? { signature } : {}
|
|
47774
|
+
}];
|
|
47775
|
+
})))
|
|
47766
47776
|
};
|
|
47767
47777
|
};
|
|
47768
|
-
const writeBundleManifest = async ({ buildPath, bundleId, targetFiles }) => {
|
|
47778
|
+
const writeBundleManifest = async ({ buildPath, bundleId, signFileHash, targetFiles }) => {
|
|
47769
47779
|
const manifest = await createBundleManifest({
|
|
47770
47780
|
bundleId,
|
|
47781
|
+
signFileHash,
|
|
47771
47782
|
targetFiles
|
|
47772
47783
|
});
|
|
47773
47784
|
const manifestPath = path$1.join(buildPath, "manifest.json");
|
|
@@ -48035,6 +48046,7 @@ const getDefaultTargetAppVersion = async (platform) => {
|
|
|
48035
48046
|
};
|
|
48036
48047
|
//#endregion
|
|
48037
48048
|
//#region src/commands/deploy.ts
|
|
48049
|
+
const compressBrotli = promisify(brotliCompress);
|
|
48038
48050
|
const normalizeRolloutPercentage = (rollout) => {
|
|
48039
48051
|
if (rollout === void 0) return 100;
|
|
48040
48052
|
const parsedRollout = typeof rollout === "number" ? rollout : Number(rollout);
|
|
@@ -48044,6 +48056,64 @@ const normalizeRolloutPercentage = (rollout) => {
|
|
|
48044
48056
|
const getRolloutCohortCountFromPercentage = (rolloutPercentage) => {
|
|
48045
48057
|
return rolloutPercentage * 10;
|
|
48046
48058
|
};
|
|
48059
|
+
const normalizePatchMaxBaseBundles = (maxBaseBundles) => {
|
|
48060
|
+
if (maxBaseBundles === void 0) return 3;
|
|
48061
|
+
if (!Number.isInteger(maxBaseBundles) || maxBaseBundles < 1) throw new Error("Patch maxBaseBundles must be a positive integer");
|
|
48062
|
+
return maxBaseBundles;
|
|
48063
|
+
};
|
|
48064
|
+
const getPatchBaseBundles = async ({ bundleId, channel, databasePlugin, maxBaseBundles, platform, target }) => {
|
|
48065
|
+
const where = {
|
|
48066
|
+
channel,
|
|
48067
|
+
enabled: true,
|
|
48068
|
+
id: { lt: bundleId },
|
|
48069
|
+
platform,
|
|
48070
|
+
...target.fingerprintHash ? { fingerprintHash: target.fingerprintHash } : {
|
|
48071
|
+
targetAppVersion: target.appVersion,
|
|
48072
|
+
targetAppVersionNotNull: true
|
|
48073
|
+
}
|
|
48074
|
+
};
|
|
48075
|
+
const { data } = await databasePlugin.getBundles({
|
|
48076
|
+
limit: maxBaseBundles,
|
|
48077
|
+
orderBy: {
|
|
48078
|
+
direction: "desc",
|
|
48079
|
+
field: "id"
|
|
48080
|
+
},
|
|
48081
|
+
where
|
|
48082
|
+
});
|
|
48083
|
+
return data.filter((bundle) => bundle.id !== bundleId).slice(0, maxBaseBundles);
|
|
48084
|
+
};
|
|
48085
|
+
const createAutoPatches = async ({ bundleId, channel, databasePlugin, maxBaseBundles, platform, storagePlugin, target }) => {
|
|
48086
|
+
const baseBundles = await getPatchBaseBundles({
|
|
48087
|
+
bundleId,
|
|
48088
|
+
channel,
|
|
48089
|
+
databasePlugin,
|
|
48090
|
+
maxBaseBundles,
|
|
48091
|
+
platform,
|
|
48092
|
+
target
|
|
48093
|
+
});
|
|
48094
|
+
const failures = [];
|
|
48095
|
+
let createdCount = 0;
|
|
48096
|
+
for (const baseBundle of baseBundles) try {
|
|
48097
|
+
await createBundleDiff({
|
|
48098
|
+
baseBundleId: baseBundle.id,
|
|
48099
|
+
bundleId
|
|
48100
|
+
}, {
|
|
48101
|
+
databasePlugin,
|
|
48102
|
+
storagePlugin
|
|
48103
|
+
}, { makePrimary: createdCount === 0 });
|
|
48104
|
+
createdCount += 1;
|
|
48105
|
+
} catch (error) {
|
|
48106
|
+
failures.push({
|
|
48107
|
+
baseBundleId: baseBundle.id,
|
|
48108
|
+
message: error instanceof Error ? error.message : "Unknown patch error"
|
|
48109
|
+
});
|
|
48110
|
+
}
|
|
48111
|
+
return {
|
|
48112
|
+
candidateCount: baseBundles.length,
|
|
48113
|
+
createdCount,
|
|
48114
|
+
failures
|
|
48115
|
+
};
|
|
48116
|
+
};
|
|
48047
48117
|
const getExtensionFromCompressStrategy = (compressStrategy) => {
|
|
48048
48118
|
switch (compressStrategy) {
|
|
48049
48119
|
case "tar.br": return ".tar.br";
|
|
@@ -48052,6 +48122,33 @@ const getExtensionFromCompressStrategy = (compressStrategy) => {
|
|
|
48052
48122
|
default: throw new Error(`Unsupported compress strategy: ${compressStrategy}`);
|
|
48053
48123
|
}
|
|
48054
48124
|
};
|
|
48125
|
+
const getRelativeStorageDir = (relativePath) => {
|
|
48126
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
48127
|
+
const dirname = path$1.posix.dirname(normalized);
|
|
48128
|
+
return dirname === "." ? "" : dirname;
|
|
48129
|
+
};
|
|
48130
|
+
const isBrotliManifestBundleAsset = (relativePath) => /(^|\/)index\.[^/]+\.bundle$/.test(relativePath.replace(/\\/g, "/"));
|
|
48131
|
+
const replaceStorageUriLeaf = (storageUri, nextLeaf) => {
|
|
48132
|
+
const storageUrl = new URL(storageUri);
|
|
48133
|
+
const normalizedPath = storageUrl.pathname.replace(/\/+$/, "");
|
|
48134
|
+
const lastSlashIndex = normalizedPath.lastIndexOf("/");
|
|
48135
|
+
storageUrl.pathname = `${lastSlashIndex >= 0 ? normalizedPath.slice(0, lastSlashIndex) : ""}/${nextLeaf}`;
|
|
48136
|
+
return storageUrl.toString();
|
|
48137
|
+
};
|
|
48138
|
+
const ensureUploadSourcePath = async ({ outputPath, targetFile }) => {
|
|
48139
|
+
const uploadName = isBrotliManifestBundleAsset(targetFile.name) ? `${targetFile.name}.br` : targetFile.name;
|
|
48140
|
+
const expectedFilename = path$1.posix.basename(uploadName);
|
|
48141
|
+
const actualFilename = path$1.basename(targetFile.path);
|
|
48142
|
+
if (uploadName === targetFile.name && expectedFilename === actualFilename) return targetFile.path;
|
|
48143
|
+
const aliasDir = path$1.join(outputPath, "upload-artifacts", getRelativeStorageDir(uploadName));
|
|
48144
|
+
await fs$2.promises.mkdir(aliasDir, { recursive: true });
|
|
48145
|
+
const aliasPath = path$1.join(aliasDir, expectedFilename);
|
|
48146
|
+
if (uploadName !== targetFile.name) {
|
|
48147
|
+
const source = await fs$2.promises.readFile(targetFile.path);
|
|
48148
|
+
await fs$2.promises.writeFile(aliasPath, await compressBrotli(source, { params: { [constants$1.BROTLI_PARAM_QUALITY]: 11 } }));
|
|
48149
|
+
} else await fs$2.promises.copyFile(targetFile.path, aliasPath);
|
|
48150
|
+
return aliasPath;
|
|
48151
|
+
};
|
|
48055
48152
|
const getPlatformName = (platform) => platform === "ios" ? "iOS" : "Android";
|
|
48056
48153
|
const getDeployPlatforms = async (options) => {
|
|
48057
48154
|
if (options.platform) return [options.platform];
|
|
@@ -48099,6 +48196,7 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
|
|
|
48099
48196
|
console.error("No config found. Please run `hot-updater init` first.");
|
|
48100
48197
|
process.exit(1);
|
|
48101
48198
|
}
|
|
48199
|
+
const maxPatchBaseBundles = config.patch.enabled ? normalizePatchMaxBaseBundles(config.patch.maxBaseBundles) : 0;
|
|
48102
48200
|
const signingValidation = await validateSigningConfig(config);
|
|
48103
48201
|
if (signingValidation.issues.length > 0) {
|
|
48104
48202
|
const errors = signingValidation.issues.filter((i) => i.type === "error");
|
|
@@ -48180,6 +48278,7 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
|
|
|
48180
48278
|
const outputPath = options.bundleOutputPath ?? HotUpdateDirUtil.getDefaultOutputPath({ cwd });
|
|
48181
48279
|
let bundleId = null;
|
|
48182
48280
|
let fileHash;
|
|
48281
|
+
let manifestFileHash = null;
|
|
48183
48282
|
const platformName = getPlatformName(platform);
|
|
48184
48283
|
const outputRoot = getBundleOutputRoot({
|
|
48185
48284
|
cwd,
|
|
@@ -48204,9 +48303,14 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
|
|
|
48204
48303
|
config.storage(),
|
|
48205
48304
|
config.database()
|
|
48206
48305
|
]);
|
|
48306
|
+
assertNodeStoragePlugin(storagePlugin);
|
|
48207
48307
|
try {
|
|
48208
48308
|
const taskRef = {
|
|
48209
48309
|
buildResult: null,
|
|
48310
|
+
targetFiles: [],
|
|
48311
|
+
manifestPath: null,
|
|
48312
|
+
manifestStorageUri: null,
|
|
48313
|
+
assetBaseStorageUri: null,
|
|
48210
48314
|
storageUri: null
|
|
48211
48315
|
};
|
|
48212
48316
|
await p.tasks([{
|
|
@@ -48222,12 +48326,15 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
|
|
|
48222
48326
|
const { manifestPath } = await writeBundleManifest({
|
|
48223
48327
|
buildPath,
|
|
48224
48328
|
bundleId: currentBundleId,
|
|
48329
|
+
signFileHash: config.signing?.enabled && config.signing.privateKeyPath ? (assetFileHash) => signBundle(assetFileHash, config.signing.privateKeyPath) : void 0,
|
|
48225
48330
|
targetFiles
|
|
48226
48331
|
});
|
|
48227
48332
|
const bundleTargetFiles = [...targetFiles, {
|
|
48228
48333
|
path: manifestPath,
|
|
48229
48334
|
name: "manifest.json"
|
|
48230
48335
|
}];
|
|
48336
|
+
taskRef.targetFiles = targetFiles;
|
|
48337
|
+
taskRef.manifestPath = manifestPath;
|
|
48231
48338
|
switch (compressStrategy) {
|
|
48232
48339
|
case "tar.br":
|
|
48233
48340
|
await createTarBrTargetFiles({
|
|
@@ -48260,6 +48367,11 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
|
|
|
48260
48367
|
throw error;
|
|
48261
48368
|
}
|
|
48262
48369
|
}
|
|
48370
|
+
manifestFileHash = await getFileHashFromFile(manifestPath);
|
|
48371
|
+
if (config.signing?.enabled) {
|
|
48372
|
+
if (!config.signing.privateKeyPath) throw new Error("privateKeyPath is required when signing is enabled. Please provide a valid path to your RSA private key in hot-updater.config.ts");
|
|
48373
|
+
manifestFileHash = createSignedFileHash(await signBundle(manifestFileHash, config.signing.privateKeyPath));
|
|
48374
|
+
}
|
|
48263
48375
|
return `✅ Build Complete (${buildPlugin.name})`;
|
|
48264
48376
|
}
|
|
48265
48377
|
}]);
|
|
@@ -48270,8 +48382,25 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
|
|
|
48270
48382
|
task: async () => {
|
|
48271
48383
|
if (!bundleId) throw new Error("Bundle ID not found");
|
|
48272
48384
|
try {
|
|
48273
|
-
const { storageUri } = await storagePlugin.upload(bundleId, bundlePath);
|
|
48385
|
+
const { storageUri } = await storagePlugin.profiles.node.upload(bundleId, bundlePath);
|
|
48274
48386
|
taskRef.storageUri = storageUri;
|
|
48387
|
+
if (!taskRef.manifestPath) throw new Error("Manifest path not found");
|
|
48388
|
+
const manifestUpload = await storagePlugin.profiles.node.upload(bundleId, taskRef.manifestPath);
|
|
48389
|
+
taskRef.manifestStorageUri = manifestUpload.storageUri;
|
|
48390
|
+
taskRef.assetBaseStorageUri = replaceStorageUriLeaf(manifestUpload.storageUri, "files");
|
|
48391
|
+
await Promise.all(taskRef.targetFiles.map(async (targetFile) => {
|
|
48392
|
+
const relativeDir = getRelativeStorageDir(isBrotliManifestBundleAsset(targetFile.name) ? `${targetFile.name}.br` : targetFile.name);
|
|
48393
|
+
const uploadKey = [
|
|
48394
|
+
bundleId,
|
|
48395
|
+
"files",
|
|
48396
|
+
relativeDir
|
|
48397
|
+
].filter(Boolean).join("/");
|
|
48398
|
+
const uploadSourcePath = await ensureUploadSourcePath({
|
|
48399
|
+
outputPath: outputRoot,
|
|
48400
|
+
targetFile
|
|
48401
|
+
});
|
|
48402
|
+
return storagePlugin.profiles.node.upload(uploadKey, uploadSourcePath);
|
|
48403
|
+
}));
|
|
48275
48404
|
} catch (e) {
|
|
48276
48405
|
if (e instanceof Error) p.log.error(e.message);
|
|
48277
48406
|
throw new Error("Failed to upload bundle to storage");
|
|
@@ -48283,6 +48412,7 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
|
|
|
48283
48412
|
task: async () => {
|
|
48284
48413
|
if (!bundleId) throw new Error("Bundle ID not found");
|
|
48285
48414
|
if (!taskRef.storageUri) throw new Error("Storage URI not found");
|
|
48415
|
+
if (!manifestFileHash) throw new Error("Manifest file hash not found");
|
|
48286
48416
|
const appVersion = await getNativeAppVersion(platform);
|
|
48287
48417
|
try {
|
|
48288
48418
|
await databasePlugin.appendBundle({
|
|
@@ -48298,6 +48428,9 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
|
|
|
48298
48428
|
fingerprintHash: target.fingerprintHash,
|
|
48299
48429
|
storageUri: taskRef.storageUri,
|
|
48300
48430
|
metadata: appVersion ? { app_version: appVersion } : {},
|
|
48431
|
+
assetBaseStorageUri: taskRef.assetBaseStorageUri,
|
|
48432
|
+
manifestFileHash,
|
|
48433
|
+
manifestStorageUri: taskRef.manifestStorageUri,
|
|
48301
48434
|
rolloutCohortCount
|
|
48302
48435
|
});
|
|
48303
48436
|
await databasePlugin.commitBundle();
|
|
@@ -48305,18 +48438,53 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
|
|
|
48305
48438
|
if (e instanceof Error) p.log.error(e.message);
|
|
48306
48439
|
throw e;
|
|
48307
48440
|
}
|
|
48308
|
-
await databasePlugin.onUnmount?.();
|
|
48309
48441
|
return `✅ Update Complete (${databasePlugin.name})`;
|
|
48310
48442
|
}
|
|
48311
48443
|
}]);
|
|
48312
48444
|
if (!bundleId) throw new Error("Bundle ID not found");
|
|
48445
|
+
const confirmedBundleId = bundleId;
|
|
48446
|
+
if (config.patch.enabled) {
|
|
48447
|
+
let patchSummary = {
|
|
48448
|
+
candidateCount: 0,
|
|
48449
|
+
createdCount: 0,
|
|
48450
|
+
failures: []
|
|
48451
|
+
};
|
|
48452
|
+
await p.tasks([{
|
|
48453
|
+
title: "⚡ Optimizing Delivery",
|
|
48454
|
+
task: async () => {
|
|
48455
|
+
try {
|
|
48456
|
+
patchSummary = await createAutoPatches({
|
|
48457
|
+
bundleId: confirmedBundleId,
|
|
48458
|
+
channel,
|
|
48459
|
+
databasePlugin,
|
|
48460
|
+
maxBaseBundles: maxPatchBaseBundles,
|
|
48461
|
+
platform,
|
|
48462
|
+
storagePlugin,
|
|
48463
|
+
target
|
|
48464
|
+
});
|
|
48465
|
+
} catch (error) {
|
|
48466
|
+
const message = error instanceof Error ? error.message : "Unknown patch optimization error";
|
|
48467
|
+
p.log.warn(`Partial updates unavailable: ${message}`);
|
|
48468
|
+
patchSummary = {
|
|
48469
|
+
candidateCount: 0,
|
|
48470
|
+
createdCount: 0,
|
|
48471
|
+
failures: []
|
|
48472
|
+
};
|
|
48473
|
+
}
|
|
48474
|
+
if (!patchSummary.candidateCount) return "Skipped (no compatible base bundles)";
|
|
48475
|
+
if (!patchSummary.createdCount) return "Skipped (no patch artifacts created)";
|
|
48476
|
+
return `✅ Prepared ${patchSummary.createdCount} partial update path(s)`;
|
|
48477
|
+
}
|
|
48478
|
+
}]);
|
|
48479
|
+
for (const failure of patchSummary.failures) p.log.warn(`Partial update skipped for ${failure.baseBundleId.slice(0, 8)}: ${failure.message}`);
|
|
48480
|
+
}
|
|
48313
48481
|
if (options.interactive) {
|
|
48314
48482
|
const port = await getConsolePort(config);
|
|
48315
48483
|
const isConsoleOpen = await isPortReachable(port, { host: "localhost" });
|
|
48316
48484
|
const openUrl = new URL(`http://localhost:${port}`);
|
|
48317
48485
|
openUrl.searchParams.set("channel", channel);
|
|
48318
48486
|
openUrl.searchParams.set("platform", platform);
|
|
48319
|
-
openUrl.searchParams.set("bundleId",
|
|
48487
|
+
openUrl.searchParams.set("bundleId", confirmedBundleId);
|
|
48320
48488
|
const url = openUrl.toString();
|
|
48321
48489
|
const note = `Console: ${url}`;
|
|
48322
48490
|
if (!isConsoleOpen) {
|
|
@@ -48331,15 +48499,15 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
|
|
|
48331
48499
|
p.note(note);
|
|
48332
48500
|
}
|
|
48333
48501
|
if (multiPlatform) {
|
|
48334
|
-
p.log.success(`✅ ${platformName} Deployment Successful (${
|
|
48502
|
+
p.log.success(`✅ ${platformName} Deployment Successful (${confirmedBundleId})`);
|
|
48335
48503
|
return {
|
|
48336
|
-
bundleId,
|
|
48504
|
+
bundleId: confirmedBundleId,
|
|
48337
48505
|
platform
|
|
48338
48506
|
};
|
|
48339
48507
|
}
|
|
48340
|
-
p.outro(`🚀 Deployment Successful (${
|
|
48508
|
+
p.outro(`🚀 Deployment Successful (${confirmedBundleId})`);
|
|
48341
48509
|
return {
|
|
48342
|
-
bundleId,
|
|
48510
|
+
bundleId: confirmedBundleId,
|
|
48343
48511
|
platform
|
|
48344
48512
|
};
|
|
48345
48513
|
} catch (e) {
|
|
@@ -48512,6 +48680,43 @@ const init = async () => {
|
|
|
48512
48680
|
] })) p.log.info(".gitignore has been modified to include hot-updater entries");
|
|
48513
48681
|
};
|
|
48514
48682
|
//#endregion
|
|
48683
|
+
//#region src/commands/patch.ts
|
|
48684
|
+
const createPatch = async (options) => {
|
|
48685
|
+
printBanner$1();
|
|
48686
|
+
const platform = options.platform ?? (options.interactive ? await getPlatform("Which platform does this patch target?") : null);
|
|
48687
|
+
if (p.isCancel(platform)) return;
|
|
48688
|
+
if (!platform) {
|
|
48689
|
+
p.log.error("Platform not found. -p <ios | android> or --platform <ios | android>");
|
|
48690
|
+
return;
|
|
48691
|
+
}
|
|
48692
|
+
const config = await loadConfig({
|
|
48693
|
+
channel: options.channel,
|
|
48694
|
+
platform
|
|
48695
|
+
});
|
|
48696
|
+
const [databasePlugin, storagePlugin] = await Promise.all([config.database(), config.storage()]);
|
|
48697
|
+
try {
|
|
48698
|
+
p.note([
|
|
48699
|
+
`Channel: ${options.channel}`,
|
|
48700
|
+
`Platform: ${platform === "ios" ? "iOS" : "Android"}`,
|
|
48701
|
+
`Base bundle: ${options.baseBundleId}`,
|
|
48702
|
+
`Target bundle: ${options.bundleId}`
|
|
48703
|
+
].join("\n"), "Patch");
|
|
48704
|
+
const updatedBundle = await createBundleDiff({
|
|
48705
|
+
baseBundleId: options.baseBundleId,
|
|
48706
|
+
bundleId: options.bundleId
|
|
48707
|
+
}, {
|
|
48708
|
+
databasePlugin,
|
|
48709
|
+
storagePlugin
|
|
48710
|
+
}, { makePrimary: true });
|
|
48711
|
+
p.outro(`⚡ Patch Ready (${updatedBundle.id})`);
|
|
48712
|
+
} catch (error) {
|
|
48713
|
+
console.error(error);
|
|
48714
|
+
process.exit(1);
|
|
48715
|
+
} finally {
|
|
48716
|
+
await databasePlugin.onUnmount?.();
|
|
48717
|
+
}
|
|
48718
|
+
};
|
|
48719
|
+
//#endregion
|
|
48515
48720
|
//#region src/commands/runNative.ts
|
|
48516
48721
|
const runNativeInternal = async ({ options, platform }) => {
|
|
48517
48722
|
printBanner$1();
|
|
@@ -48695,6 +48900,10 @@ const formatBundleSummary = (bundle, nextEnabled) => {
|
|
|
48695
48900
|
].filter((line) => line !== null);
|
|
48696
48901
|
return ui.block("Bundle", lines);
|
|
48697
48902
|
};
|
|
48903
|
+
const parseTargetCohorts = (value) => {
|
|
48904
|
+
if (value === void 0) return null;
|
|
48905
|
+
return value.split(",").map((cohort) => cohort.trim()).filter(Boolean);
|
|
48906
|
+
};
|
|
48698
48907
|
const refuseNonInteractiveMutation = (action) => {
|
|
48699
48908
|
p.log.error(`Cannot ${action} a bundle without confirmation in a non-interactive shell. Re-run with -y, or use a TTY.`);
|
|
48700
48909
|
process.exit(1);
|
|
@@ -48723,6 +48932,24 @@ const handleBundleList = async (options = {}) => {
|
|
|
48723
48932
|
await safeOnUnmount$2(databasePlugin);
|
|
48724
48933
|
}
|
|
48725
48934
|
};
|
|
48935
|
+
const handleBundleShow = async (bundleId, options = {}) => {
|
|
48936
|
+
if (!options.json) printBanner$1();
|
|
48937
|
+
const databasePlugin = await (await loadConfig(null)).database();
|
|
48938
|
+
try {
|
|
48939
|
+
const bundle = await databasePlugin.getBundleById(bundleId);
|
|
48940
|
+
if (!bundle) {
|
|
48941
|
+
p.log.error(`No bundle with id ${bundleId}.`);
|
|
48942
|
+
process.exit(1);
|
|
48943
|
+
}
|
|
48944
|
+
if (options.json) {
|
|
48945
|
+
console.log(JSON.stringify(bundle, null, 2));
|
|
48946
|
+
return;
|
|
48947
|
+
}
|
|
48948
|
+
p.log.message(formatBundleSummary(bundle));
|
|
48949
|
+
} finally {
|
|
48950
|
+
await safeOnUnmount$2(databasePlugin);
|
|
48951
|
+
}
|
|
48952
|
+
};
|
|
48726
48953
|
const handleBundleSetEnabled = async (bundleId, nextEnabled, options = {}) => {
|
|
48727
48954
|
const action = nextEnabled ? "enable" : "disable";
|
|
48728
48955
|
printBanner$1();
|
|
@@ -48764,6 +48991,87 @@ const handleBundleSetEnabled = async (bundleId, nextEnabled, options = {}) => {
|
|
|
48764
48991
|
await safeOnUnmount$2(databasePlugin);
|
|
48765
48992
|
}
|
|
48766
48993
|
};
|
|
48994
|
+
const handleBundleUpdate = async (bundleId, options = {}) => {
|
|
48995
|
+
if (!options.json) printBanner$1();
|
|
48996
|
+
const targetCohorts = parseTargetCohorts(options.targetCohorts);
|
|
48997
|
+
const patch = {};
|
|
48998
|
+
if (options.rolloutCohortCount !== void 0) patch.rolloutCohortCount = options.rolloutCohortCount;
|
|
48999
|
+
if (options.forceUpdate !== void 0) patch.shouldForceUpdate = options.forceUpdate;
|
|
49000
|
+
if (targetCohorts !== null) patch.targetCohorts = targetCohorts;
|
|
49001
|
+
else if (options.clearTargetCohorts) patch.targetCohorts = null;
|
|
49002
|
+
if (Object.keys(patch).length === 0) {
|
|
49003
|
+
p.log.error("No bundle update fields were provided.");
|
|
49004
|
+
process.exit(1);
|
|
49005
|
+
}
|
|
49006
|
+
const databasePlugin = await (await loadConfig(null)).database();
|
|
49007
|
+
try {
|
|
49008
|
+
const bundle = await databasePlugin.getBundleById(bundleId);
|
|
49009
|
+
if (!bundle) {
|
|
49010
|
+
p.log.error(`No bundle with id ${bundleId}.`);
|
|
49011
|
+
process.exit(1);
|
|
49012
|
+
}
|
|
49013
|
+
if (!options.json) p.log.message(formatBundleSummary(bundle));
|
|
49014
|
+
if (!options.yes) {
|
|
49015
|
+
if (!process.stdin.isTTY) refuseNonInteractiveMutation("update");
|
|
49016
|
+
const confirmed = await p.confirm({
|
|
49017
|
+
message: "Update this bundle?",
|
|
49018
|
+
initialValue: false
|
|
49019
|
+
});
|
|
49020
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
49021
|
+
p.log.info("Aborted.");
|
|
49022
|
+
process.exit(2);
|
|
49023
|
+
}
|
|
49024
|
+
}
|
|
49025
|
+
await databasePlugin.updateBundle(bundleId, patch);
|
|
49026
|
+
await databasePlugin.commitBundle();
|
|
49027
|
+
const refetched = await databasePlugin.getBundleById(bundleId);
|
|
49028
|
+
if (!refetched) {
|
|
49029
|
+
p.log.error(`Verification failed: ${bundleId} is missing after update.`);
|
|
49030
|
+
process.exit(1);
|
|
49031
|
+
}
|
|
49032
|
+
if (options.json) {
|
|
49033
|
+
console.log(JSON.stringify(refetched, null, 2));
|
|
49034
|
+
return;
|
|
49035
|
+
}
|
|
49036
|
+
p.log.success("Updated bundle.");
|
|
49037
|
+
p.log.info(` ${ui.id(bundleId)}`);
|
|
49038
|
+
} finally {
|
|
49039
|
+
await safeOnUnmount$2(databasePlugin);
|
|
49040
|
+
}
|
|
49041
|
+
};
|
|
49042
|
+
const handleBundleDelete = async (bundleId, options = {}) => {
|
|
49043
|
+
printBanner$1();
|
|
49044
|
+
const databasePlugin = await (await loadConfig(null)).database();
|
|
49045
|
+
try {
|
|
49046
|
+
const bundle = await databasePlugin.getBundleById(bundleId);
|
|
49047
|
+
if (!bundle) {
|
|
49048
|
+
p.log.info(`No bundle with id ${bundleId}. No changes.`);
|
|
49049
|
+
return;
|
|
49050
|
+
}
|
|
49051
|
+
p.log.message(formatBundleSummary(bundle));
|
|
49052
|
+
if (!options.yes) {
|
|
49053
|
+
if (!process.stdin.isTTY) refuseNonInteractiveMutation("delete");
|
|
49054
|
+
const confirmed = await p.confirm({
|
|
49055
|
+
message: "Delete this bundle record?",
|
|
49056
|
+
initialValue: false
|
|
49057
|
+
});
|
|
49058
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
49059
|
+
p.log.info("Aborted.");
|
|
49060
|
+
process.exit(2);
|
|
49061
|
+
}
|
|
49062
|
+
}
|
|
49063
|
+
await databasePlugin.deleteBundle(bundle);
|
|
49064
|
+
await databasePlugin.commitBundle();
|
|
49065
|
+
if (await databasePlugin.getBundleById(bundleId)) {
|
|
49066
|
+
p.log.error(`Verification failed: ${bundleId} still exists.`);
|
|
49067
|
+
process.exit(1);
|
|
49068
|
+
}
|
|
49069
|
+
p.log.success("Deleted bundle record.");
|
|
49070
|
+
p.log.info(` ${ui.id(bundleId)}`);
|
|
49071
|
+
} finally {
|
|
49072
|
+
await safeOnUnmount$2(databasePlugin);
|
|
49073
|
+
}
|
|
49074
|
+
};
|
|
48767
49075
|
//#endregion
|
|
48768
49076
|
//#region ../../node_modules/.pnpm/es-toolkit@1.32.0/node_modules/es-toolkit/dist/predicate/isPlainObject.mjs
|
|
48769
49077
|
function isPlainObject(value) {
|
|
@@ -49402,6 +49710,11 @@ var import_semver = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
|
|
|
49402
49710
|
rcompareIdentifiers: identifiers.rcompareIdentifiers
|
|
49403
49711
|
};
|
|
49404
49712
|
})))(), 1);
|
|
49713
|
+
const INFRASTRUCTURE_RECOVERY_COMMANDS = [
|
|
49714
|
+
"hot-updater init",
|
|
49715
|
+
"hot-updater db migrate",
|
|
49716
|
+
"hot-updater db generate"
|
|
49717
|
+
];
|
|
49405
49718
|
const INFRASTRUCTURE_UPDATE_TARGETS = [
|
|
49406
49719
|
{
|
|
49407
49720
|
version: "0.13.0",
|
|
@@ -49422,6 +49735,10 @@ const INFRASTRUCTURE_UPDATE_TARGETS = [
|
|
|
49422
49735
|
{
|
|
49423
49736
|
version: "0.30.0",
|
|
49424
49737
|
note: "Target cohort rollout behavior"
|
|
49738
|
+
},
|
|
49739
|
+
{
|
|
49740
|
+
version: "0.31.0",
|
|
49741
|
+
note: "Bundle artifact storage fields"
|
|
49425
49742
|
}
|
|
49426
49743
|
];
|
|
49427
49744
|
const getInfrastructureTargetVersionAt = (index) => {
|
|
@@ -49463,6 +49780,7 @@ function resolveVersionEndpoint(serverBaseUrl) {
|
|
|
49463
49780
|
url.pathname = `${pathname}/version`;
|
|
49464
49781
|
return url.toString();
|
|
49465
49782
|
}
|
|
49783
|
+
const createInfrastructureRemediation = () => ({ commands: [...INFRASTRUCTURE_RECOVERY_COMMANDS] });
|
|
49466
49784
|
async function checkInfrastructureStatus({ serverBaseUrl, fetchImpl = fetch, requiredVersion = getRequiredInfrastructureVersion() }) {
|
|
49467
49785
|
const versionEndpoint = resolveVersionEndpoint(serverBaseUrl);
|
|
49468
49786
|
const baseUrl = serverBaseUrl.trim();
|
|
@@ -49546,11 +49864,14 @@ async function doctor(options = {}) {
|
|
|
49546
49864
|
packageJsonPath,
|
|
49547
49865
|
installedHotUpdaterPackages: hotUpdaterPackages
|
|
49548
49866
|
};
|
|
49549
|
-
if (serverBaseUrl)
|
|
49550
|
-
|
|
49551
|
-
|
|
49552
|
-
|
|
49553
|
-
|
|
49867
|
+
if (serverBaseUrl) {
|
|
49868
|
+
details.infrastructure = await checkInfrastructureStatus({
|
|
49869
|
+
serverBaseUrl,
|
|
49870
|
+
fetchImpl,
|
|
49871
|
+
requiredVersion: getRequiredInfrastructureVersion(hotUpdaterVersion)
|
|
49872
|
+
});
|
|
49873
|
+
if (details.infrastructure.error !== void 0 || details.infrastructure.needsUpdate === true) details.infrastructure.remediation = createInfrastructureRemediation();
|
|
49874
|
+
}
|
|
49554
49875
|
if (versionMismatches.length > 0) details.versionMismatches = versionMismatches;
|
|
49555
49876
|
const hasInfrastructureIssue = details.infrastructure?.error !== void 0 || details.infrastructure?.needsUpdate === true;
|
|
49556
49877
|
if (versionMismatches.length > 0 || hasInfrastructureIssue) return {
|
|
@@ -49620,6 +49941,12 @@ const handleDoctor = async ({ serverBaseUrl } = {}) => {
|
|
|
49620
49941
|
if (infrastructure.updateReason) p.log.info(`Reason: ${infrastructure.updateReason}`);
|
|
49621
49942
|
} else if (infrastructure.error) p.log.error(`Infrastructure check failed: ${infrastructure.error}`);
|
|
49622
49943
|
else p.log.success("Infrastructure is up to date.");
|
|
49944
|
+
if (infrastructure.remediation) p.log.message(ui.block("Recovery", [ui.kv("Managed", ui.command("hot-updater init")), ui.kv("@hot-updater/server (self-hosted)", ui.line([
|
|
49945
|
+
ui.command("hot-updater db generate"),
|
|
49946
|
+
"or",
|
|
49947
|
+
ui.command("hot-updater db migrate"),
|
|
49948
|
+
"then redeploy server"
|
|
49949
|
+
]))]));
|
|
49623
49950
|
}
|
|
49624
49951
|
if (details?.versionMismatches && details.versionMismatches.length > 0) {
|
|
49625
49952
|
p.log.warn("Version mismatches found:");
|
|
@@ -50209,12 +50536,11 @@ async function generateStandaloneSQL(options) {
|
|
|
50209
50536
|
const s = p.spinner();
|
|
50210
50537
|
s.start("Generating SQL from database schema");
|
|
50211
50538
|
const db = new Kysely({ dialect: createDialect(dbType) });
|
|
50212
|
-
const [{
|
|
50213
|
-
const
|
|
50539
|
+
const [{ createHotUpdater }, { kyselyAdapter }] = await Promise.all([import("@hot-updater/server"), import("@hot-updater/server/adapters/kysely")]);
|
|
50540
|
+
const result = await createHotUpdater({ database: kyselyAdapter({
|
|
50214
50541
|
db,
|
|
50215
50542
|
provider: dbType
|
|
50216
|
-
})
|
|
50217
|
-
const result = await HotUpdaterDB.client(adapter).createMigrator().migrateToLatest({
|
|
50543
|
+
}) }).createMigrator().migrateToLatest({
|
|
50218
50544
|
mode: "from-schema",
|
|
50219
50545
|
updateSettings: false
|
|
50220
50546
|
});
|
|
@@ -50734,6 +51060,7 @@ const handlePromote = async (bundleId, options) => {
|
|
|
50734
51060
|
let storagePlugin = null;
|
|
50735
51061
|
try {
|
|
50736
51062
|
storagePlugin = await config.storage();
|
|
51063
|
+
assertNodeStoragePlugin(storagePlugin);
|
|
50737
51064
|
} catch {
|
|
50738
51065
|
storagePlugin = null;
|
|
50739
51066
|
}
|
|
@@ -50910,6 +51237,16 @@ const handleRollback = async (channel, options = {}) => {
|
|
|
50910
51237
|
//#endregion
|
|
50911
51238
|
//#region src/index.ts
|
|
50912
51239
|
const DEFAULT_CHANNEL = "production";
|
|
51240
|
+
const parseBooleanOption = (value) => {
|
|
51241
|
+
if (value === "true") return true;
|
|
51242
|
+
if (value === "false") return false;
|
|
51243
|
+
throw new InvalidArgumentError("must be true or false");
|
|
51244
|
+
};
|
|
51245
|
+
const parseRolloutCohortCount = (value) => {
|
|
51246
|
+
const count = Number.parseInt(value, 10);
|
|
51247
|
+
if (!Number.isInteger(count) || count < 0 || count > 1e3) throw new InvalidArgumentError("must be an integer between 0 and 1000");
|
|
51248
|
+
return count;
|
|
51249
|
+
};
|
|
50913
51250
|
const program = new Command();
|
|
50914
51251
|
program.name("hot-updater").description(banner(version)).version(version);
|
|
50915
51252
|
program.command("init").description("Initialize Hot Updater").action(init);
|
|
@@ -50926,8 +51263,11 @@ bundleCommand.command("list").description("List bundles, most recent first").opt
|
|
|
50926
51263
|
if (!Number.isInteger(n) || n <= 0) throw new InvalidArgumentError("must be a positive integer");
|
|
50927
51264
|
return n;
|
|
50928
51265
|
}, 20).action(handleBundleList);
|
|
51266
|
+
bundleCommand.command("show").description("Show one bundle by id").argument("<bundle-id>", "the bundle id to show").option("--json", "output raw bundle data as JSON").action((bundleId, options) => handleBundleShow(bundleId, options));
|
|
50929
51267
|
bundleCommand.command("disable").description("Disable a bundle by id").argument("<bundle-id>", "the id of the bundle to disable").option("-y, --yes", "skip confirmation prompt").action((bundleId, options) => handleBundleSetEnabled(bundleId, false, options));
|
|
50930
51268
|
bundleCommand.command("enable").description("Re-enable a previously disabled bundle by id").argument("<bundle-id>", "the id of the bundle to enable").option("-y, --yes", "skip confirmation prompt").action((bundleId, options) => handleBundleSetEnabled(bundleId, true, options));
|
|
51269
|
+
bundleCommand.command("update").description("Update bundle rollout and targeting metadata").argument("<bundle-id>", "the bundle id to update").option("--rollout-cohort-count <count>", "rollout cohort count from 0 to 1000", parseRolloutCohortCount).option("--force-update <value>", "set force update flag (true or false)", parseBooleanOption).option("--target-cohorts <cohorts>", "comma-separated target cohorts").option("--clear-target-cohorts", "clear target cohorts").option("--json", "output the updated bundle as JSON").option("-y, --yes", "skip confirmation prompt").action(handleBundleUpdate);
|
|
51270
|
+
bundleCommand.command("delete").description("Delete a bundle record by id").argument("<bundle-id>", "the bundle id to delete").option("-y, --yes", "skip confirmation prompt").action((bundleId, options) => handleBundleDelete(bundleId, options));
|
|
50931
51271
|
bundleCommand.command("promote").description("Move or copy a bundle to a different channel").argument("<bundle-id>", "the id of the bundle to promote").requiredOption("-t, --target <channel>", "channel to promote the bundle to").addOption(new Option("-a, --action <action>", "promote action (copy creates a new bundle id; move keeps the id)").choices(["copy", "move"]).default("copy")).option("-y, --yes", "skip confirmation prompt").action((bundleId, options) => handlePromote(bundleId, options));
|
|
50932
51272
|
const keysCommand = program.command("keys").description("Code signing key management");
|
|
50933
51273
|
keysCommand.command("generate").description("Generate RSA key pair for code signing").option("-o, --output <dir>", "output directory for keys", "./keys").option("-k, --key-size <size>", "key size (2048 or 4096)", (value) => {
|
|
@@ -50954,6 +51294,9 @@ program.command("deploy").description("deploy a new version").addOption(platform
|
|
|
50954
51294
|
process.exit(1);
|
|
50955
51295
|
}
|
|
50956
51296
|
}).default(100)).addOption(interactiveCommandOption).addOption(new Option("-c, --channel <channel>", "specify the channel to deploy").default(DEFAULT_CHANNEL)).addOption(new Option("-m, --message <message>", "Specify a custom message for this deployment. If not provided, the latest git commit message will be used as the deployment message")).action(async (options) => deploy(options));
|
|
51297
|
+
program.command("patch").description("create patch artifacts for a deployed bundle").requiredOption("-b, --bundle-id <bundleId>", "target bundle id that should receive the patch artifact").requiredOption("--base-bundle-id <baseBundleId>", "older bundle id to use as the patch base").addOption(platformCommandOption).addOption(interactiveCommandOption).addOption(new Option("-c, --channel <channel>", "specify the channel used to load config").default(DEFAULT_CHANNEL)).action(async (options) => {
|
|
51298
|
+
await createPatch(options);
|
|
51299
|
+
});
|
|
50957
51300
|
program.command("rollback").description("Disable the most recent enabled bundle on a channel").argument("<channel>", "the channel to roll back").addOption(platformCommandOption).option("-y, --yes", "skip confirmation prompt").option("--target <bundle-id>", "scope rollback to exactly this bundle id (use to retry a failed rollback)").action((channel, options) => handleRollback(channel, options));
|
|
50958
51301
|
program.command("console").description("open the console").action(async () => {
|
|
50959
51302
|
printBanner$1();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hot-updater",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.31.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.19.0"
|
|
7
7
|
},
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
"jiti": "2.6.1",
|
|
50
50
|
"kysely": "0.28.16",
|
|
51
51
|
"sql-formatter": "15.6.10",
|
|
52
|
-
"@hot-updater/cli-tools": "0.
|
|
53
|
-
"@hot-updater/
|
|
54
|
-
"@hot-updater/console": "0.
|
|
55
|
-
"@hot-updater/core": "0.
|
|
56
|
-
"@hot-updater/
|
|
57
|
-
"@hot-updater/
|
|
58
|
-
"@hot-updater/server": "0.
|
|
52
|
+
"@hot-updater/cli-tools": "0.31.0",
|
|
53
|
+
"@hot-updater/android-helper": "0.31.0",
|
|
54
|
+
"@hot-updater/console": "0.31.0",
|
|
55
|
+
"@hot-updater/core": "0.31.0",
|
|
56
|
+
"@hot-updater/plugin-core": "0.31.0",
|
|
57
|
+
"@hot-updater/apple-helper": "0.31.0",
|
|
58
|
+
"@hot-updater/server": "0.31.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@bacons/xcode": "1.0.0-alpha.24",
|
|
@@ -74,12 +74,12 @@
|
|
|
74
74
|
"plist": "^3.1.0",
|
|
75
75
|
"semver": "^7.6.3",
|
|
76
76
|
"typescript": "6.0.2",
|
|
77
|
-
"@hot-updater/aws": "0.
|
|
78
|
-
"@hot-updater/
|
|
79
|
-
"@hot-updater/
|
|
80
|
-
"@hot-updater/
|
|
81
|
-
"@hot-updater/
|
|
82
|
-
"@hot-updater/
|
|
77
|
+
"@hot-updater/aws": "0.31.0",
|
|
78
|
+
"@hot-updater/cloudflare": "0.31.0",
|
|
79
|
+
"@hot-updater/firebase": "0.31.0",
|
|
80
|
+
"@hot-updater/server": "0.31.0",
|
|
81
|
+
"@hot-updater/supabase": "0.31.0",
|
|
82
|
+
"@hot-updater/test-utils": "0.31.0"
|
|
83
83
|
},
|
|
84
84
|
"peerDependencies": {
|
|
85
85
|
"@hot-updater/aws": "*",
|