@hot-updater/cli-tools 0.30.12 → 0.31.1

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.d.mts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Readable } from "stream";
2
2
  import { Key as Key$1 } from "node:readline";
3
3
  import { Readable as Readable$1, Writable } from "node:stream";
4
- import { Bundle, ConfigInput, DatabasePlugin, Platform, RequiredDeep, StoragePlugin } from "@hot-updater/plugin-core";
4
+ import * as _$_hot_updater_core0 from "@hot-updater/core";
5
+ import { Bundle, ConfigInput, DatabasePlugin, NodeStoragePlugin, Platform, RequiredDeep } from "@hot-updater/plugin-core";
5
6
 
6
7
  //#region src/BuildLogger.d.ts
7
8
  type LinePattern = string | RegExp;
@@ -366,14 +367,6 @@ declare const makeEnv: (newEnvVars: Record<string, EnvVarValue>, filePath?: stri
366
367
  preserveKeys?: string[];
367
368
  }) => Promise<string>;
368
369
  //#endregion
369
- //#region ../core/dist/index.d.mts
370
- //#endregion
371
- //#region src/types.d.ts
372
- type Platform$1 = "ios" | "android";
373
- type BundleMetadata = {
374
- app_version?: string;
375
- };
376
- //#endregion
377
370
  //#region src/promoteBundle.d.ts
378
371
  declare const LEGACY_BUNDLE_ERROR = "This OTA bundle was created by a version that does not support manifest.json. Copy bundle is not available.";
379
372
  interface PromoteBundleInput {
@@ -385,7 +378,7 @@ interface PromoteBundleInput {
385
378
  interface PromoteBundleDependencies {
386
379
  config: ConfigResponse;
387
380
  databasePlugin: DatabasePlugin;
388
- storagePlugin: StoragePlugin | null;
381
+ storagePlugin: NodeStoragePlugin | null;
389
382
  }
390
383
  declare function createCopiedBundleArchive({
391
384
  bundle,
@@ -397,23 +390,34 @@ declare function createCopiedBundleArchive({
397
390
  bundle: Bundle;
398
391
  config: ConfigResponse;
399
392
  nextBundleId: string;
400
- storagePlugin: StoragePlugin;
393
+ storagePlugin: NodeStoragePlugin;
401
394
  targetChannel: string;
402
395
  }): Promise<{
403
- id: string;
404
- channel: string;
405
- storageUri: string;
406
- fileHash: string;
407
- platform: Platform$1;
408
- shouldForceUpdate: boolean;
409
- enabled: boolean;
410
- gitCommitHash: string | null;
411
- message: string | null;
412
- targetAppVersion: string | null;
413
- fingerprintHash: string | null;
414
- metadata?: BundleMetadata;
415
- rolloutCohortCount?: number | null;
416
- targetCohorts?: string[] | null;
396
+ bundle: {
397
+ id: string;
398
+ channel: string;
399
+ storageUri: string;
400
+ fileHash: string;
401
+ metadata: _$_hot_updater_core0.BundleMetadata | undefined;
402
+ assetBaseStorageUri: string;
403
+ patches: never[];
404
+ patchBaseBundleId: null;
405
+ manifestFileHash: string;
406
+ manifestStorageUri: string;
407
+ patchBaseFileHash: null;
408
+ patchFileHash: null;
409
+ patchStorageUri: null;
410
+ platform: _$_hot_updater_core0.Platform;
411
+ shouldForceUpdate: boolean;
412
+ enabled: boolean;
413
+ gitCommitHash: string | null;
414
+ message: string | null;
415
+ targetAppVersion: string | null;
416
+ fingerprintHash: string | null;
417
+ rolloutCohortCount?: number | null;
418
+ targetCohorts?: string[] | null;
419
+ };
420
+ uploadedStorageUris: string[];
417
421
  }>;
418
422
  declare function promoteBundle({
419
423
  action,
package/dist/index.mjs CHANGED
@@ -34,6 +34,7 @@ import { Buffer as Buffer$2 } from "node:buffer";
34
34
  import ts from "typescript";
35
35
  import { loadConfig as loadConfig$1 } from "unconfig";
36
36
  import { brotliDecompressSync } from "node:zlib";
37
+ import { getManifestFileHash, stripBundleArtifactMetadata } from "@hot-updater/core";
37
38
  import { createUUIDv7, detectCompressionFormat } from "@hot-updater/plugin-core";
38
39
  import { transformSync } from "oxc-transform";
39
40
  //#endregion
@@ -42930,6 +42931,10 @@ const getDefaultConfig = () => {
42930
42931
  updateStrategy: "appVersion",
42931
42932
  compressStrategy: "zip",
42932
42933
  fingerprint: { extraSources: [] },
42934
+ patch: {
42935
+ enabled: true,
42936
+ maxBaseBundles: 3
42937
+ },
42933
42938
  console: { port: 1422 },
42934
42939
  platform: getDefaultPlatformConfig(),
42935
42940
  nativeBuild: {
@@ -43064,6 +43069,18 @@ function getArchiveFilename(storageUri) {
43064
43069
  const { pathname } = new URL(storageUri);
43065
43070
  return path$1.basename(pathname) || "bundle.zip";
43066
43071
  }
43072
+ const getRelativeStorageDir = (relativePath) => {
43073
+ const normalized = relativePath.replace(/\\/g, "/");
43074
+ const dirname = path$1.posix.dirname(normalized);
43075
+ return dirname === "." ? "" : dirname;
43076
+ };
43077
+ const replaceStorageUriLeaf = (storageUri, nextLeaf) => {
43078
+ const storageUrl = new URL(storageUri);
43079
+ const normalizedPath = storageUrl.pathname.replace(/\/+$/, "");
43080
+ const lastSlashIndex = normalizedPath.lastIndexOf("/");
43081
+ storageUrl.pathname = `${lastSlashIndex >= 0 ? normalizedPath.slice(0, lastSlashIndex) : ""}/${nextLeaf}`;
43082
+ return storageUrl.toString();
43083
+ };
43067
43084
  function resolveExtractedPath(rootDir, entryName) {
43068
43085
  const normalizedEntryName = entryName.replaceAll("\\", "/");
43069
43086
  const entryPath = path$1.resolve(rootDir, normalizedEntryName);
@@ -43071,11 +43088,25 @@ function resolveExtractedPath(rootDir, entryName) {
43071
43088
  if (relativePath.startsWith("..") || path$1.isAbsolute(relativePath) || normalizedEntryName.startsWith("/")) throw new Error(`Invalid archive entry path: ${entryName}`);
43072
43089
  return entryPath;
43073
43090
  }
43074
- async function downloadArchive(fileUrl, archivePath) {
43091
+ async function downloadArchive(storageUri, storagePlugin, archivePath) {
43092
+ const protocol = new URL(storageUri).protocol.replace(":", "");
43093
+ if (protocol === "http" || protocol === "https") {
43094
+ const archiveBuffer = await downloadFromUrl(storageUri);
43095
+ await fs$3.writeFile(archivePath, archiveBuffer);
43096
+ return;
43097
+ }
43098
+ await downloadFromStorage(storageUri, storagePlugin, archivePath);
43099
+ }
43100
+ async function downloadFromUrl(fileUrl) {
43075
43101
  const response = await fetch(fileUrl);
43076
43102
  if (!response.ok) throw new Error(`Failed to download bundle archive: ${response.statusText}`);
43077
- const archiveBuffer = Buffer.from(await response.arrayBuffer());
43078
- await fs$3.writeFile(archivePath, archiveBuffer);
43103
+ return new Uint8Array(await response.arrayBuffer());
43104
+ }
43105
+ async function downloadFromStorage(storageUri, storagePlugin, filePath) {
43106
+ if (!storagePlugin) throw new Error("Storage plugin is not configured");
43107
+ const protocol = new URL(storageUri).protocol.replace(":", "");
43108
+ if (storagePlugin.supportedProtocol !== protocol) throw new Error(`No storage plugin for protocol: ${protocol}`);
43109
+ await storagePlugin.profiles.node.downloadFile(storageUri, filePath);
43079
43110
  }
43080
43111
  async function extractZipArchive(archivePath, extractDir) {
43081
43112
  const zip = await import_lib.default.loadAsync(await fs$3.readFile(archivePath));
@@ -43165,41 +43196,68 @@ async function rewriteManifestBundleId(extractDir, nextBundleId) {
43165
43196
  const manifest = JSON.parse(await fs$3.readFile(manifestPath, "utf8"));
43166
43197
  manifest.bundleId = nextBundleId;
43167
43198
  await fs$3.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
43168
- }
43169
- async function resolveBundleDownloadUrl(storageUri, storagePlugin) {
43170
- const protocol = new URL(storageUri).protocol.replace(":", "");
43171
- if (protocol === "http" || protocol === "https") return storageUri;
43172
- if (!storagePlugin) throw new Error("Storage plugin is not configured");
43173
- if (storagePlugin.supportedProtocol !== protocol) throw new Error(`No storage plugin for protocol: ${protocol}`);
43174
- const { fileUrl } = await storagePlugin.getDownloadUrl(storageUri);
43175
- if (!fileUrl) throw new Error("Storage plugin returned empty fileUrl");
43176
- return fileUrl;
43199
+ return {
43200
+ manifest,
43201
+ manifestPath
43202
+ };
43177
43203
  }
43178
43204
  async function createCopiedBundleArchive({ bundle, config, nextBundleId, storagePlugin, targetChannel }) {
43179
- const downloadUrl = await resolveBundleDownloadUrl(bundle.storageUri, storagePlugin);
43180
43205
  const archiveFilename = getArchiveFilename(bundle.storageUri);
43181
43206
  const workDir = await fs$3.mkdtemp(path$1.join(os.tmpdir(), "hot-updater-console-promote-"));
43182
43207
  const sourceArchivePath = path$1.join(workDir, archiveFilename);
43183
43208
  const extractDir = path$1.join(workDir, "bundle");
43184
43209
  const outputArchivePath = path$1.join(workDir, archiveFilename);
43210
+ const uploadedStorageUris = [];
43185
43211
  await fs$3.mkdir(extractDir, { recursive: true });
43186
43212
  try {
43187
- await downloadArchive(downloadUrl, sourceArchivePath);
43213
+ await downloadArchive(bundle.storageUri, storagePlugin, sourceArchivePath);
43188
43214
  const format = await extractArchive(sourceArchivePath, extractDir);
43189
- await rewriteManifestBundleId(extractDir, nextBundleId);
43215
+ const { manifest, manifestPath } = await rewriteManifestBundleId(extractDir, nextBundleId);
43190
43216
  await fs$3.rm(sourceArchivePath, { force: true });
43191
43217
  await createArchiveFromDirectory(extractDir, outputArchivePath, format);
43192
43218
  const fileHash = await getFileHash(outputArchivePath);
43193
- if (isSignedFileHash(bundle.fileHash) && !config.signing?.enabled) throw new Error("Cannot copy a signed bundle without signing.privateKeyPath in hot-updater.config.ts");
43194
- const nextFileHash = config.signing?.enabled && config.signing.privateKeyPath ? await signFileHash(fileHash, config.signing.privateKeyPath) : fileHash;
43195
- const { storageUri } = await storagePlugin.upload(nextBundleId, outputArchivePath);
43219
+ const manifestHash = await getFileHash(manifestPath);
43220
+ if ([bundle.fileHash, getManifestFileHash(bundle)].filter((hash) => Boolean(hash)).some((hash) => isSignedFileHash(hash)) && !config.signing?.privateKeyPath) throw new Error("Cannot copy a signed bundle without signing.privateKeyPath in hot-updater.config.ts");
43221
+ const signingKeyPath = config.signing?.enabled && config.signing.privateKeyPath ? config.signing.privateKeyPath : null;
43222
+ const nextFileHash = signingKeyPath ? await signFileHash(fileHash, signingKeyPath) : fileHash;
43223
+ const nextManifestFileHash = signingKeyPath ? await signFileHash(manifestHash, signingKeyPath) : manifestHash;
43224
+ const archiveUpload = await storagePlugin.profiles.node.upload(nextBundleId, outputArchivePath);
43225
+ uploadedStorageUris.push(archiveUpload.storageUri);
43226
+ const manifestUpload = await storagePlugin.profiles.node.upload(nextBundleId, manifestPath);
43227
+ uploadedStorageUris.push(manifestUpload.storageUri);
43228
+ const assetPaths = Object.keys(manifest.assets ?? {}).sort((left, right) => left.localeCompare(right));
43229
+ for (const assetPath of assetPaths) {
43230
+ const uploadKey = [
43231
+ nextBundleId,
43232
+ "files",
43233
+ getRelativeStorageDir(assetPath)
43234
+ ].filter(Boolean).join("/");
43235
+ const assetUpload = await storagePlugin.profiles.node.upload(uploadKey, path$1.join(extractDir, assetPath));
43236
+ uploadedStorageUris.push(assetUpload.storageUri);
43237
+ }
43238
+ const assetBaseStorageUri = replaceStorageUriLeaf(manifestUpload.storageUri, "files");
43196
43239
  return {
43197
- ...bundle,
43198
- id: nextBundleId,
43199
- channel: targetChannel,
43200
- storageUri,
43201
- fileHash: nextFileHash
43240
+ bundle: {
43241
+ ...bundle,
43242
+ id: nextBundleId,
43243
+ channel: targetChannel,
43244
+ storageUri: archiveUpload.storageUri,
43245
+ fileHash: nextFileHash,
43246
+ metadata: stripBundleArtifactMetadata(bundle.metadata),
43247
+ assetBaseStorageUri,
43248
+ patches: [],
43249
+ patchBaseBundleId: null,
43250
+ manifestFileHash: nextManifestFileHash,
43251
+ manifestStorageUri: manifestUpload.storageUri,
43252
+ patchBaseFileHash: null,
43253
+ patchFileHash: null,
43254
+ patchStorageUri: null
43255
+ },
43256
+ uploadedStorageUris
43202
43257
  };
43258
+ } catch (error) {
43259
+ await deleteUploadedCopy(storagePlugin, uploadedStorageUris);
43260
+ throw error;
43203
43261
  } finally {
43204
43262
  await fs$3.rm(workDir, {
43205
43263
  recursive: true,
@@ -43207,10 +43265,10 @@ async function createCopiedBundleArchive({ bundle, config, nextBundleId, storage
43207
43265
  });
43208
43266
  }
43209
43267
  }
43210
- async function deleteUploadedCopy(storagePlugin, storageUri) {
43211
- if (!storageUri) return;
43212
- try {
43213
- await storagePlugin.delete(storageUri);
43268
+ async function deleteUploadedCopy(storagePlugin, storageUris) {
43269
+ if (storageUris.length === 0) return;
43270
+ for (const storageUri of new Set(storageUris)) try {
43271
+ await storagePlugin.profiles.node.delete(storageUri);
43214
43272
  } catch (error) {
43215
43273
  console.error("Failed to delete uploaded bundle copy:", error);
43216
43274
  }
@@ -43230,21 +43288,21 @@ async function promoteBundle({ action, bundleId, nextBundleId, targetChannel },
43230
43288
  }
43231
43289
  if (!deps.storagePlugin) throw new Error("Storage plugin is not configured");
43232
43290
  const resolvedNextBundleId = nextBundleId?.trim() || createUUIDv7();
43233
- const copiedBundle = await createCopiedBundleArchive({
43291
+ const { bundle: copiedBundle, uploadedStorageUris } = await createCopiedBundleArchive({
43234
43292
  bundle,
43235
43293
  config: deps.config,
43236
43294
  nextBundleId: resolvedNextBundleId,
43237
43295
  storagePlugin: deps.storagePlugin,
43238
43296
  targetChannel: normalizedTargetChannel
43239
43297
  });
43240
- let uploadedStorageUri = copiedBundle.storageUri;
43298
+ let shouldCleanupUploadedCopy = true;
43241
43299
  try {
43242
43300
  await deps.databasePlugin.appendBundle(copiedBundle);
43243
43301
  await deps.databasePlugin.commitBundle();
43244
- uploadedStorageUri = null;
43302
+ shouldCleanupUploadedCopy = false;
43245
43303
  return copiedBundle;
43246
43304
  } catch (error) {
43247
- await deleteUploadedCopy(deps.storagePlugin, uploadedStorageUri);
43305
+ if (shouldCleanupUploadedCopy) await deleteUploadedCopy(deps.storagePlugin, uploadedStorageUris);
43248
43306
  throw error;
43249
43307
  }
43250
43308
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/cli-tools",
3
- "version": "0.30.12",
3
+ "version": "0.31.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
@@ -46,7 +46,8 @@
46
46
  "oxc-transform": "0.121.0",
47
47
  "typescript": "6.0.2",
48
48
  "unconfig": "7.5.0",
49
- "@hot-updater/plugin-core": "0.30.12"
49
+ "@hot-updater/core": "0.31.1",
50
+ "@hot-updater/plugin-core": "0.31.1"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@clack/prompts": "1.0.1",
@@ -61,7 +62,7 @@
61
62
  "semver": "^7.6.3",
62
63
  "tar": "^7.5.1",
63
64
  "workspace-tools": "^0.36.4",
64
- "@hot-updater/test-utils": "0.30.12"
65
+ "@hot-updater/test-utils": "0.31.1"
65
66
  },
66
67
  "inlinedDependencies": {
67
68
  "@babel/code-frame": "7.29.0",
@@ -78,7 +79,7 @@
78
79
  "ansi-align": "3.0.1",
79
80
  "ansi-regex": [
80
81
  "5.0.1",
81
- "6.1.0"
82
+ "6.2.2"
82
83
  ],
83
84
  "ansi-styles": "6.2.1",
84
85
  "boxen": "8.0.1",