@hot-updater/cli-tools 1.0.0-rc.14 → 1.0.0-rc.16

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
@@ -2,6 +2,7 @@ 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
4
  import { Bundle, BundleSigningPlugin, ConfigInput, LocalSigningConfig, Platform, RequiredDeep, SigningConfig, StoragePluginWith, StoragePutResult } from "@hot-updater/plugin-core";
5
+ import { ManifestArchive } from "@hot-updater/core";
5
6
  //#region src/BuildLogger.d.ts
6
7
  type LinePattern = string | RegExp;
7
8
  interface BuildLoggerConfig {
@@ -163,41 +164,16 @@ declare const copyDirToTmp: (dir: string, childDirname?: string) => Promise<{
163
164
  }>;
164
165
  //#endregion
165
166
  //#region src/createTarBr.d.ts
166
- declare const createTarBrTargetFiles: ({ outfile, targetFiles }: {
167
- targetFiles: {
168
- path: string;
169
- name: string;
170
- }[];
171
- outfile: string;
172
- }) => Promise<string>;
173
- declare const createTarBr: ({ outfile, targetDir, excludeExts }: {
174
- targetDir: string;
167
+ /** Build the one supported bulk transport from the exact manifest file set. */
168
+ declare function createTarBrTargetFiles({ outfile, targetFiles }: {
175
169
  outfile: string;
176
- excludeExts?: string[];
177
- }) => Promise<string>;
178
- //#endregion
179
- //#region src/createTarGz.d.ts
180
- declare const createTarGzTargetFiles: ({ outfile, targetFiles }: {
181
170
  targetFiles: {
182
171
  path: string;
183
172
  name: string;
184
173
  }[];
185
- outfile: string;
186
- }) => Promise<string>;
187
- declare const createTarGz: ({ outfile, targetDir, excludeExts }: {
188
- targetDir: string;
189
- outfile: string;
190
- excludeExts?: string[];
191
- }) => Promise<string>;
174
+ }): Promise<ManifestArchive>;
192
175
  //#endregion
193
176
  //#region src/createZip.d.ts
194
- declare const createZipTargetFiles: ({ outfile, targetFiles }: {
195
- targetFiles: {
196
- path: string;
197
- name: string;
198
- }[];
199
- outfile: string;
200
- }) => Promise<string>;
201
177
  declare const createZip: ({ outfile, targetDir, excludeExts }: {
202
178
  targetDir: string;
203
179
  outfile: string;
@@ -295,6 +271,33 @@ declare const readHotUpdaterInitEnv: (cwd: string, envFile?: string) => Promise<
295
271
  declare const readHotUpdaterEnv: (cwd: string) => Promise<Readonly<Record<string, string>>>;
296
272
  declare const getHotUpdaterEnvValue: (env: Readonly<Record<string, string>>, key: string) => string | undefined;
297
273
  //#endregion
274
+ //#region src/hotUpdaterPlugins.d.ts
275
+ /** Where `hot-updater init` writes a managed server's plugin list. */
276
+ declare const HOT_UPDATER_PLUGINS_PATH = "hotUpdater.plugins.ts";
277
+ /**
278
+ * The plugins file for a managed provider: a re-export of the fixed list its
279
+ * package ships, which its prebuilt server runs too.
280
+ */
281
+ declare const renderHotUpdaterPlugins: (pkg: string) => string;
282
+ type WriteHotUpdaterPluginsResult = {
283
+ readonly status: "created" | "unchanged";
284
+ readonly path: string;
285
+ } | {
286
+ readonly status: "skipped";
287
+ readonly path: string;
288
+ readonly reason: string;
289
+ };
290
+ /** Writes the plugins file; an existing file that differs is kept. */
291
+ declare const writeHotUpdaterPlugins: (pkg: string, filePath?: string) => Promise<WriteHotUpdaterPluginsResult>;
292
+ /** Writes the plugins file for `pkg` and reports what happened. */
293
+ declare const generateHotUpdaterPlugins: (pkg: string, filePath?: string) => Promise<WriteHotUpdaterPluginsResult>;
294
+ /**
295
+ * The plugins a project's `hotUpdater.plugins.ts` exports, which its server
296
+ * runs; undefined when the project has no such file. The console assembles
297
+ * them in process to read Insights and API keys as the server would.
298
+ */
299
+ declare const loadHotUpdaterPlugins: (cwd?: string) => Promise<readonly unknown[] | undefined>;
300
+ //#endregion
298
301
  //#region src/HotUpdateDirUtil.d.ts
299
302
  declare const HotUpdateDirUtil: {
300
303
  readonly dirName: ".hot-updater";
@@ -467,7 +470,7 @@ declare const makeEnv: (newEnvVars: Record<string, EnvVarValue>, filePath?: stri
467
470
  //#region src/promoteBundle.d.ts
468
471
  type PromoteStoragePlugin = StoragePluginWith<"get" | "put" | "exists" | "delete">;
469
472
  declare const LEGACY_BUNDLE_ERROR = "This OTA bundle was created by a version that does not support manifest.json. Copy bundle is not available.";
470
- declare function createCopiedBundleArchive({ bundle, config, nextBundleId, storagePlugin }: {
473
+ declare function createCopiedBundleArtifacts({ bundle, config, nextBundleId, storagePlugin }: {
471
474
  bundle: Bundle;
472
475
  config: ConfigResponse;
473
476
  nextBundleId: string;
@@ -475,9 +478,6 @@ declare function createCopiedBundleArchive({ bundle, config, nextBundleId, stora
475
478
  }): Promise<{
476
479
  bundle: {
477
480
  id: string;
478
- archiveByteSize: number;
479
- storageUri: string;
480
- fileHash: string;
481
481
  metadata: import("@hot-updater/core").BundleMetadata | undefined;
482
482
  assetBaseStorageUri: string;
483
483
  patches: never[];
@@ -1733,4 +1733,4 @@ type TransformTemplateArgs<T extends string> = { [Key in ExtractPlaceholders<T>]
1733
1733
  */
1734
1734
  declare function transformTemplate<T extends string>(templateString: T, values: TransformTemplateArgs<T>): string;
1735
1735
  //#endregion
1736
- export { BuildLogger, BuildLoggerConfig, BuildType, BundleSigningSession, ConfigBuilder, ConfigBuilderScaffold, ConfigResponse, CreateHotUpdaterConfigScaffoldFromBuilderOptions, CreateHotUpdaterConfigScaffoldOptions, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, HotUpdaterConfigOptions, HotUpdaterConfigScaffold, HotUpdaterInitEnv, HotUpdaterLogWriter, IConfigBuilder, ImportInfo, InitEnvFileError, InitError, InitProviderDefinition, InitProviderInputDefinition, InitProviderInputPersistence, LEGACY_BUNDLE_ERROR, LegacyInfrastructureError, ManagedHelperStatement, ManagedHelperStrategy, MissingInitInputsError, PromptProgress, PromptSpinner, ProviderConfig, ReactNativeMetadata, ReadPackageUpResult, RunInitOptions, WriteHotUpdaterConfigResult, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, banner, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCopiedBundleArchive, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBr, createTarBrTargetFiles, createTarGz, createTarGzTargetFiles, createZip, createZipTargetFiles, decryptJson, defineInitProvider, encryptJson, ensureInstallPackages, formatApiKeyNote, getAndroidSdkPath, getBundleSigningPublicKey, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, getStorageFileByteSize, link, loadConfig, log, makeEnv, p, prepareBundleSigning, printBanner, putStorageFile, readBundleSigningPublicKeyFile, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderImportStatements, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolvePackageVersion, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig, writeStorageFile, writeStorageResponseFile };
1736
+ export { BuildLogger, BuildLoggerConfig, BuildType, BundleSigningSession, ConfigBuilder, ConfigBuilderScaffold, ConfigResponse, CreateHotUpdaterConfigScaffoldFromBuilderOptions, CreateHotUpdaterConfigScaffoldOptions, HOT_UPDATER_PLUGINS_PATH, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, HotUpdaterConfigOptions, HotUpdaterConfigScaffold, HotUpdaterInitEnv, HotUpdaterLogWriter, IConfigBuilder, ImportInfo, InitEnvFileError, InitError, InitProviderDefinition, InitProviderInputDefinition, InitProviderInputPersistence, LEGACY_BUNDLE_ERROR, LegacyInfrastructureError, ManagedHelperStatement, ManagedHelperStrategy, MissingInitInputsError, PromptProgress, PromptSpinner, ProviderConfig, ReactNativeMetadata, ReadPackageUpResult, RunInitOptions, WriteHotUpdaterConfigResult, WriteHotUpdaterPluginsResult, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, banner, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCopiedBundleArtifacts, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBrTargetFiles, createZip, decryptJson, defineInitProvider, encryptJson, ensureInstallPackages, formatApiKeyNote, generateHotUpdaterPlugins, getAndroidSdkPath, getBundleSigningPublicKey, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, getStorageFileByteSize, link, loadConfig, loadHotUpdaterPlugins, log, makeEnv, p, prepareBundleSigning, printBanner, putStorageFile, readBundleSigningPublicKeyFile, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderHotUpdaterPlugins, renderImportStatements, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolvePackageVersion, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig, writeHotUpdaterPlugins, writeStorageFile, writeStorageResponseFile };
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  import path, { dirname, parse } from "path";
4
4
  import { createInterface } from "readline";
5
5
  import { PassThrough } from "stream";
6
- import fs, { createWriteStream } from "fs";
6
+ import fs from "fs";
7
7
  import fs$1 from "fs/promises";
8
8
  import $r, { EventEmitter } from "events";
9
9
  import Pi from "assert";
@@ -14,28 +14,27 @@ import process$1, { execArgv, execPath, hrtime, platform, stdin, stdout } from "
14
14
  import * as l from "node:readline";
15
15
  import l__default from "node:readline";
16
16
  import tty, { ReadStream } from "node:tty";
17
- import Ei, { appendFileSync, createReadStream, createWriteStream as createWriteStream$1, existsSync, lstatSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
17
+ import Ei, { appendFileSync, createReadStream, createWriteStream, existsSync, lstatSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
18
18
  import path$1, { basename, dirname as dirname$1, join, posix, win32 } from "node:path";
19
19
  import os, { constants } from "node:os";
20
- import crypto$1, { randomBytes, randomUUID } from "node:crypto";
20
+ import crypto$1, { createHash, randomBytes, randomUUID } from "node:crypto";
21
21
  import fs$2 from "node:fs/promises";
22
- import { pipeline } from "stream/promises";
23
- import * as Ms from "zlib";
24
- import Jr, { constants as constants$1, createBrotliCompress } from "zlib";
25
- import { EventEmitter as EventEmitter$1, addAbortListener, on, once, setMaxListeners } from "node:events";
26
22
  import As, { Duplex, PassThrough as PassThrough$1, Readable as Readable$1, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
23
+ import { finished, pipeline } from "node:stream/promises";
24
+ import { brotliDecompressSync, constants as constants$1, createBrotliCompress } from "node:zlib";
25
+ import { EventEmitter as EventEmitter$1, addAbortListener, on, once, setMaxListeners } from "node:events";
27
26
  import { StringDecoder } from "node:string_decoder";
27
+ import * as Ms from "zlib";
28
+ import Jr from "zlib";
28
29
  import no from "node:assert";
29
30
  import { fileURLToPath } from "node:url";
30
31
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
31
32
  import { scheduler, setImmediate as setImmediate$1, setTimeout as setTimeout$1 } from "node:timers/promises";
32
33
  import { serialize } from "node:v8";
33
- import { finished, pipeline as pipeline$1 } from "node:stream/promises";
34
34
  import { Buffer as Buffer$2 } from "node:buffer";
35
35
  import { parseSync } from "oxc-parser";
36
- import { createBundleStorageKey, createDatabasePlugin, createStoragePlugin, createStorageRootUriWithPath, detectCompressionFormat, getContentType, getManifestAssetDownloadPath, getManifestAssetStoragePath, isContentAddressedAssetFileHash, parseStorageUri, resolveManifestAssetStorageUri } from "@hot-updater/plugin-core";
37
36
  import { loadConfig as loadConfig$1 } from "unconfig";
38
- import { brotliDecompressSync, constants as constants$2, createBrotliCompress as createBrotliCompress$1 } from "node:zlib";
37
+ import { createBundleStorageKey, createStoragePlugin, createStorageRootUriWithPath, getContentType, getManifestAssetDownloadPath, getManifestAssetStoragePath, isContentAddressedAssetFileHash, resolveManifestAssetStorageUri } from "@hot-updater/plugin-core";
39
38
  import { getManifestFileHash, stripBundleArtifactMetadata } from "@hot-updater/core";
40
39
  import { transformSync } from "oxc-transform";
41
40
  //#endregion
@@ -25043,14 +25042,14 @@ var ConfigBuilder = class {
25043
25042
  intermediateCode = "";
25044
25043
  collectedImports = /* @__PURE__ */ new Map();
25045
25044
  constructor() {
25046
- this.addImport({
25047
- pkg: "dotenv",
25048
- named: ["config"]
25049
- });
25050
25045
  this.addImport({
25051
25046
  pkg: "hot-updater",
25052
25047
  named: ["defineConfig"]
25053
25048
  });
25049
+ this.addImport({
25050
+ pkg: "node:fs",
25051
+ named: ["existsSync"]
25052
+ });
25054
25053
  }
25055
25054
  addImport(info) {
25056
25055
  const pkg = info.pkg;
@@ -25127,7 +25126,9 @@ var ConfigBuilder = class {
25127
25126
  const text = `
25128
25127
  ${importStatements}
25129
25128
 
25130
- config({ path: ".env.hotupdater" });
25129
+ if (existsSync(".env.hotupdater")) {
25130
+ process.loadEnvFile(".env.hotupdater");
25131
+ }
25131
25132
 
25132
25133
  ${this.intermediateCode ? `${this.intermediateCode}\n` : ""}
25133
25134
  export default defineConfig({
@@ -28325,7 +28326,7 @@ var fo = (s, t) => {
28325
28326
  });
28326
28327
  });
28327
28328
  };
28328
- var uo = K(co, fo, (s) => new Te(s), (s) => new Xt(s), (s, t) => {
28329
+ K(co, fo, (s) => new Te(s), (s) => new Xt(s), (s, t) => {
28329
28330
  t?.length && Xi(s, t);
28330
28331
  });
28331
28332
  var mo = (s, t) => {
@@ -28439,150 +28440,47 @@ var Ro = (s) => {
28439
28440
  };
28440
28441
  //#endregion
28441
28442
  //#region src/createTarBr.ts
28442
- const createTarBrTargetFiles = async ({ outfile, targetFiles }) => {
28443
- await fs$1.rm(outfile, { force: true });
28444
- const tmpDir = path.join(path.dirname(outfile), `.tmp-tar-${Date.now()}`);
28445
- await fs$1.mkdir(tmpDir, { recursive: true });
28443
+ /** Build the one supported bulk transport from the exact manifest file set. */
28444
+ async function createTarBrTargetFiles({ outfile, targetFiles }) {
28445
+ await fs$2.mkdir(path$1.dirname(outfile), { recursive: true });
28446
+ const staging = await fs$2.mkdtemp(path$1.join(path$1.dirname(outfile), ".tar-br-"));
28446
28447
  try {
28448
+ const names = /* @__PURE__ */ new Set();
28447
28449
  for (const target of targetFiles) {
28448
- const sourcePath = target.path;
28449
- const destPath = path.join(tmpDir, target.name);
28450
- await fs$1.mkdir(path.dirname(destPath), { recursive: true });
28451
- if ((await fs$1.stat(sourcePath)).isDirectory()) await copyDir$1(sourcePath, destPath);
28452
- else await fs$1.copyFile(sourcePath, destPath);
28453
- }
28454
- await fs$1.mkdir(path.dirname(outfile), { recursive: true });
28450
+ const name = target.name;
28451
+ if (!name || name !== name.trim() || name.includes("\\") || name.includes("\0") || name.startsWith("/") || /^[A-Za-z]:/.test(name) || name.split("/").some((part) => !part || part === "." || part === "..") || name === "manifest.json" || name === "bundle.tar.br" || names.has(name)) throw new Error(`Invalid or duplicate archive asset path: ${name}`);
28452
+ if (!(await fs$2.lstat(target.path)).isFile()) throw new Error(`Archive asset is not a regular file: ${name}`);
28453
+ names.add(name);
28454
+ const destination = path$1.join(staging, name);
28455
+ await fs$2.mkdir(path$1.dirname(destination), { recursive: true });
28456
+ await fs$2.copyFile(target.path, destination);
28457
+ await fs$2.chmod(destination, 420);
28458
+ }
28459
+ let tarByteSize = 0;
28460
+ const countTarBytes = new Transform({ transform(chunk, _encoding, callback) {
28461
+ tarByteSize += chunk.length;
28462
+ callback(null, chunk);
28463
+ } });
28455
28464
  await pipeline(Kn({
28456
- cwd: tmpDir,
28465
+ cwd: staging,
28457
28466
  portable: true,
28458
28467
  mtime: /* @__PURE__ */ new Date(0),
28459
28468
  gzip: false
28460
- }, await fs$1.readdir(tmpDir)), createBrotliCompress({ params: {
28461
- [constants$1.BROTLI_PARAM_QUALITY]: 11,
28462
- [constants$1.BROTLI_PARAM_LGWIN]: 24
28463
- } }), createWriteStream(outfile));
28464
- return outfile;
28465
- } finally {
28466
- await fs$1.rm(tmpDir, {
28467
- recursive: true,
28468
- force: true
28469
- });
28470
- }
28471
- };
28472
- async function copyDir$1(src, dest) {
28473
- await fs$1.mkdir(dest, { recursive: true });
28474
- const entries = await fs$1.readdir(src, { withFileTypes: true });
28475
- for (const entry of entries) {
28476
- const srcPath = path.join(src, entry.name);
28477
- const destPath = path.join(dest, entry.name);
28478
- if (entry.isDirectory()) await copyDir$1(srcPath, destPath);
28479
- else await fs$1.copyFile(srcPath, destPath);
28480
- }
28481
- }
28482
- const createBrotliTar = async ({ cwd, files, outfile }) => {
28483
- await fs$1.mkdir(path.dirname(outfile), { recursive: true });
28484
- await pipeline(Kn({
28485
- cwd,
28486
- portable: true,
28487
- mtime: /* @__PURE__ */ new Date(0),
28488
- gzip: false
28489
- }, files), createBrotliCompress({ params: {
28490
- [constants$1.BROTLI_PARAM_QUALITY]: 11,
28491
- [constants$1.BROTLI_PARAM_LGWIN]: 24
28492
- } }), createWriteStream(outfile));
28493
- };
28494
- const createTarBr = async ({ outfile, targetDir, excludeExts = [] }) => {
28495
- await fs$1.rm(outfile, { force: true });
28496
- async function getFiles(dir, baseDir = "") {
28497
- const entries = await fs$1.readdir(dir, { withFileTypes: true });
28498
- const files = [];
28499
- for (const entry of entries) {
28500
- if (excludeExts.some((pattern) => entry.name.includes(pattern))) continue;
28501
- const fullPath = path.join(dir, entry.name);
28502
- const relativePath = path.join(baseDir, entry.name);
28503
- if (entry.isDirectory()) {
28504
- const subFiles = await getFiles(fullPath, relativePath);
28505
- files.push(...subFiles);
28506
- } else files.push(relativePath);
28507
- }
28508
- return files;
28509
- }
28510
- const filesToInclude = await getFiles(targetDir);
28511
- filesToInclude.sort();
28512
- await createBrotliTar({
28513
- cwd: targetDir,
28514
- files: filesToInclude,
28515
- outfile
28516
- });
28517
- return outfile;
28518
- };
28519
- //#endregion
28520
- //#region src/createTarGz.ts
28521
- const createTarGzTargetFiles = async ({ outfile, targetFiles }) => {
28522
- await fs$1.rm(outfile, { force: true });
28523
- const tmpDir = path.join(path.dirname(outfile), `.tmp-tar-${Date.now()}`);
28524
- await fs$1.mkdir(tmpDir, { recursive: true });
28525
- try {
28526
- for (const target of targetFiles) {
28527
- const sourcePath = target.path;
28528
- const destPath = path.join(tmpDir, target.name);
28529
- await fs$1.mkdir(path.dirname(destPath), { recursive: true });
28530
- if ((await fs$1.stat(sourcePath)).isDirectory()) await copyDir(sourcePath, destPath);
28531
- else await fs$1.copyFile(sourcePath, destPath);
28532
- }
28533
- await fs$1.mkdir(path.dirname(outfile), { recursive: true });
28534
- await Kn({
28535
- file: outfile,
28536
- cwd: tmpDir,
28537
- portable: true,
28538
- mtime: /* @__PURE__ */ new Date(0),
28539
- gzip: true
28540
- }, await fs$1.readdir(tmpDir));
28541
- return outfile;
28469
+ }, [...names].sort()), countTarBytes, createBrotliCompress({ params: { [constants$1.BROTLI_PARAM_QUALITY]: 11 } }), createWriteStream(outfile));
28470
+ const hash = createHash("sha256");
28471
+ for await (const chunk of createReadStream(outfile)) hash.update(chunk);
28472
+ return {
28473
+ downloadFileHash: hash.digest("hex"),
28474
+ downloadByteSize: (await fs$2.stat(outfile)).size,
28475
+ tarByteSize
28476
+ };
28542
28477
  } finally {
28543
- await fs$1.rm(tmpDir, {
28478
+ await fs$2.rm(staging, {
28544
28479
  recursive: true,
28545
28480
  force: true
28546
28481
  });
28547
28482
  }
28548
- };
28549
- async function copyDir(src, dest) {
28550
- await fs$1.mkdir(dest, { recursive: true });
28551
- const entries = await fs$1.readdir(src, { withFileTypes: true });
28552
- for (const entry of entries) {
28553
- const srcPath = path.join(src, entry.name);
28554
- const destPath = path.join(dest, entry.name);
28555
- if (entry.isDirectory()) await copyDir(srcPath, destPath);
28556
- else await fs$1.copyFile(srcPath, destPath);
28557
- }
28558
28483
  }
28559
- const createTarGz = async ({ outfile, targetDir, excludeExts = [] }) => {
28560
- await fs$1.rm(outfile, { force: true });
28561
- async function getFiles(dir, baseDir = "") {
28562
- const entries = await fs$1.readdir(dir, { withFileTypes: true });
28563
- const files = [];
28564
- for (const entry of entries) {
28565
- if (excludeExts.some((pattern) => entry.name.includes(pattern))) continue;
28566
- const fullPath = path.join(dir, entry.name);
28567
- const relativePath = path.join(baseDir, entry.name);
28568
- if (entry.isDirectory()) {
28569
- const subFiles = await getFiles(fullPath, relativePath);
28570
- files.push(...subFiles);
28571
- } else files.push(relativePath);
28572
- }
28573
- return files;
28574
- }
28575
- const filesToInclude = await getFiles(targetDir);
28576
- filesToInclude.sort();
28577
- await Kn({
28578
- file: outfile,
28579
- cwd: targetDir,
28580
- portable: true,
28581
- mtime: /* @__PURE__ */ new Date(0),
28582
- gzip: true
28583
- }, filesToInclude);
28584
- return outfile;
28585
- };
28586
28484
  //#endregion
28587
28485
  //#region ../../node_modules/.pnpm/process-nextick-args@2.0.1/node_modules/process-nextick-args/index.js
28588
28486
  var require_process_nextick_args = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -37664,41 +37562,6 @@ var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((export
37664
37562
  JSZip.external = require_external();
37665
37563
  module.exports = JSZip;
37666
37564
  })))(), 1);
37667
- const createZipTargetFiles = async ({ outfile, targetFiles }) => {
37668
- const zip = new import_lib.default();
37669
- await fs$1.rm(outfile, { force: true });
37670
- async function addFiles(dir, zipFolder) {
37671
- const files = await fs$1.readdir(dir);
37672
- files.sort();
37673
- for (const file of files) {
37674
- const fullPath = path.join(dir, file);
37675
- if ((await fs$1.stat(fullPath)).isDirectory()) {
37676
- const folder = zipFolder.folder(file);
37677
- if (!folder) continue;
37678
- await addFiles(fullPath, folder);
37679
- } else {
37680
- const data = await fs$1.readFile(fullPath);
37681
- zipFolder.file(file, data);
37682
- }
37683
- }
37684
- }
37685
- for (const target of targetFiles) if ((await fs$1.stat(target.path)).isDirectory()) {
37686
- const folder = zip.folder(target.name);
37687
- if (folder) await addFiles(target.path, folder);
37688
- } else {
37689
- const data = await fs$1.readFile(target.path);
37690
- zip.file(target.name, data);
37691
- }
37692
- const content = await zip.generateAsync({
37693
- type: "nodebuffer",
37694
- compression: "DEFLATE",
37695
- compressionOptions: { level: 9 },
37696
- platform: "UNIX"
37697
- });
37698
- await fs$1.mkdir(path.dirname(outfile), { recursive: true });
37699
- await fs$1.writeFile(outfile, content);
37700
- return outfile;
37701
- };
37702
37565
  const createZip = async ({ outfile, targetDir, excludeExts = [] }) => {
37703
37566
  const zip = new import_lib.default();
37704
37567
  await fs$1.rm(outfile, { force: true });
@@ -42298,8 +42161,8 @@ const addPropertiesAsync = {
42298
42161
  },
42299
42162
  output: {
42300
42163
  ...addProperties,
42301
- fileUrl: ({ value }) => ({ stream: createWriteStream$1(value) }),
42302
- filePath: ({ value: { file, append } }) => ({ stream: createWriteStream$1(file, append ? { flags: "a" } : {}) }),
42164
+ fileUrl: ({ value }) => ({ stream: createWriteStream(value) }),
42165
+ filePath: ({ value: { file, append } }) => ({ stream: createWriteStream(file, append ? { flags: "a" } : {}) }),
42303
42166
  webStream: ({ value }) => ({ stream: Writable.fromWeb(value) }),
42304
42167
  iterable: forbiddenIfAsync,
42305
42168
  asyncIterable: forbiddenIfAsync,
@@ -44175,7 +44038,6 @@ const getReactNativeMetadatas = (cwd) => {
44175
44038
  const HOT_UPDATER_CONFIG_PATH = "hot-updater.config.ts";
44176
44039
  const CONFIG_FILE_NAME = "hot-updater.config.ts";
44177
44040
  const MANAGED_IMPORT_PACKAGES = /* @__PURE__ */ new Set([
44178
- "dotenv",
44179
44041
  "firebase-admin",
44180
44042
  "firebase-admin/app",
44181
44043
  "hot-updater",
@@ -44428,7 +44290,6 @@ const updateManagedObject = (existing, next) => {
44428
44290
  for (const edit of propertyEdits.sort((left, right) => right.start - left.start)) mergedText = mergedText.slice(0, edit.start) + edit.text + mergedText.slice(edit.end);
44429
44291
  return appendMissingProperties(mergedText, missingPropertyTexts, existing.objectExpression.properties.length > 0);
44430
44292
  };
44431
- const isConfigCallStatement = (statement) => statement.type === "ExpressionStatement" && statement.expression.type === "CallExpression" && statement.expression.callee.type === "Identifier" && statement.expression.callee.name === "config";
44432
44293
  const getManagedHelperName = (statement) => {
44433
44294
  if (statement.type !== "VariableDeclaration") return null;
44434
44295
  const declaration = statement.declarations[0];
@@ -44436,16 +44297,20 @@ const getManagedHelperName = (statement) => {
44436
44297
  return declaration.id.name;
44437
44298
  };
44438
44299
  const rebuildImportBlock = (source, scaffold) => {
44300
+ const imports = scaffold.imports.map((info) => info.pkg === "node:fs" ? {
44301
+ ...info,
44302
+ named: info.named?.filter((name) => name !== "existsSync")
44303
+ } : info);
44439
44304
  const importDeclarations = source.program.body.filter((statement) => statement.type === "ImportDeclaration");
44440
44305
  const firstImport = importDeclarations[0];
44441
44306
  const lastImport = importDeclarations.at(-1);
44442
44307
  if (!firstImport || !lastImport) return {
44443
44308
  start: 0,
44444
44309
  end: 0,
44445
- text: `${renderImportStatements(scaffold.imports)}\n\n`
44310
+ text: `${renderImportStatements(imports)}\n\n`
44446
44311
  };
44447
44312
  const preservedImportTexts = importDeclarations.filter((declaration) => !MANAGED_IMPORT_PACKAGES.has(declaration.source.value)).map((declaration) => source.text.slice(getTopLevelFullStart(source, declaration), declaration.end).trim());
44448
- const managedImportText = renderImportStatements(scaffold.imports);
44313
+ const managedImportText = renderImportStatements(imports);
44449
44314
  const nextImportBlock = [...preservedImportTexts, managedImportText].filter(Boolean).join("\n");
44450
44315
  return {
44451
44316
  start: getTopLevelFullStart(source, firstImport),
@@ -44458,12 +44323,7 @@ const rebuildManagedBody = (source, exportStart, scaffold) => {
44458
44323
  const managedHelpers = new Map(scaffold.helperStatements.map((statement) => [statement.name, statement]));
44459
44324
  const emittedHelpers = /* @__PURE__ */ new Set();
44460
44325
  const bodyStatements = [];
44461
- const configStatements = [];
44462
44326
  for (const statement of statementsBeforeExport) {
44463
- if (isConfigCallStatement(statement)) {
44464
- configStatements.push(getStatementText(source, statement));
44465
- continue;
44466
- }
44467
44327
  const helperName = getManagedHelperName(statement);
44468
44328
  if (!helperName || !MANAGED_HELPER_NAMES.has(helperName)) {
44469
44329
  bodyStatements.push(getStatementText(source, statement));
@@ -44478,8 +44338,7 @@ const rebuildManagedBody = (source, exportStart, scaffold) => {
44478
44338
  }
44479
44339
  for (const helper of scaffold.helperStatements) if (!emittedHelpers.has(helper.name)) bodyStatements.push(helper.code.trim());
44480
44340
  const bodyText = bodyStatements.filter(Boolean).join("\n\n");
44481
- const configStatement = configStatements.join("\n\n") || `config({ path: ".env.hotupdater" });`;
44482
- const managedBody = bodyText ? `\n\n${configStatement}\n\n${bodyText}\n\n` : `\n\n${configStatement}\n\n`;
44341
+ const managedBody = bodyText ? `\n\n${bodyText}\n\n` : "\n\n";
44483
44342
  return {
44484
44343
  start: source.program.body.filter((statement) => statement.type === "ImportDeclaration").at(-1)?.end ?? 0,
44485
44344
  end: exportStart,
@@ -44678,6 +44537,77 @@ const getHotUpdaterEnvValue = (env, key) => {
44678
44537
  return env[key]?.trim() || void 0;
44679
44538
  };
44680
44539
  //#endregion
44540
+ //#region src/hotUpdaterPlugins.ts
44541
+ /** Where `hot-updater init` writes a managed server's plugin list. */
44542
+ const HOT_UPDATER_PLUGINS_PATH = "hotUpdater.plugins.ts";
44543
+ /**
44544
+ * The plugins file for a managed provider: a re-export of the fixed list its
44545
+ * package ships, which its prebuilt server runs too.
44546
+ */
44547
+ const renderHotUpdaterPlugins = (pkg) => [
44548
+ "// Generated by `hot-updater init`. The managed server runs these plugins,",
44549
+ `// and ${pkg} fixes the list, so the server, CLI, and console agree.`,
44550
+ `export { plugins } from "${pkg}";`,
44551
+ ""
44552
+ ].join("\n");
44553
+ /** Writes the plugins file; an existing file that differs is kept. */
44554
+ const writeHotUpdaterPlugins = async (pkg, filePath = HOT_UPDATER_PLUGINS_PATH) => {
44555
+ const text = renderHotUpdaterPlugins(pkg);
44556
+ const existingText = await fs$1.readFile(filePath, "utf-8").catch((error) => {
44557
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
44558
+ throw error;
44559
+ });
44560
+ if (existingText === null) {
44561
+ await fs$1.writeFile(filePath, text, "utf-8");
44562
+ return {
44563
+ status: "created",
44564
+ path: filePath
44565
+ };
44566
+ }
44567
+ if (existingText === text) return {
44568
+ status: "unchanged",
44569
+ path: filePath
44570
+ };
44571
+ return {
44572
+ status: "skipped",
44573
+ path: filePath,
44574
+ reason: `it does not re-export the plugins ${pkg} runs; replace it with \`export { plugins } from "${pkg}";\``
44575
+ };
44576
+ };
44577
+ /** Writes the plugins file for `pkg` and reports what happened. */
44578
+ const generateHotUpdaterPlugins = async (pkg, filePath = HOT_UPDATER_PLUGINS_PATH) => {
44579
+ const result = await writeHotUpdaterPlugins(pkg, filePath);
44580
+ if (result.status === "created") p.log.success(`Generated '${filePath}' with the plugins the server runs.`);
44581
+ else if (result.status === "skipped") p.log.warn(`Kept existing '${filePath}' unchanged: ${result.reason}`);
44582
+ return result;
44583
+ };
44584
+ /**
44585
+ * The plugins a project's `hotUpdater.plugins.ts` exports, which its server
44586
+ * runs; undefined when the project has no such file. The console assembles
44587
+ * them in process to read Insights and API keys as the server would.
44588
+ */
44589
+ const loadHotUpdaterPlugins = async (cwd = getCwd()) => {
44590
+ const { config, sources } = await loadConfig$1({
44591
+ cwd,
44592
+ stopAt: path.dirname(cwd),
44593
+ merge: false,
44594
+ sources: [{
44595
+ files: "hotUpdater.plugins",
44596
+ extensions: [
44597
+ "ts",
44598
+ "mts",
44599
+ "cts",
44600
+ "js",
44601
+ "mjs",
44602
+ "cjs"
44603
+ ]
44604
+ }]
44605
+ });
44606
+ if (sources.length === 0) return void 0;
44607
+ if (!Array.isArray(config?.plugins)) throw new Error(`${path.basename(sources[0])} must export plugins, an array of plugins.`);
44608
+ return config.plugins;
44609
+ };
44610
+ //#endregion
44681
44611
  //#region src/infrastructureGeneration.ts
44682
44612
  const assertInfrastructureGenerationPayload = ({ payload, provider, resource }) => {
44683
44613
  if (typeof payload !== "object" || payload === null || !("infrastructureGeneration" in payload) || payload.infrastructureGeneration !== 1) throw new LegacyInfrastructureError(provider, resource);
@@ -44873,89 +44803,20 @@ function isMergeableValue(value) {
44873
44803
  //#endregion
44874
44804
  //#region src/loadConfig.ts
44875
44805
  var import_out = /* @__PURE__ */ __toESM(require_out(), 1);
44876
- const missingDatabase = createDatabasePlugin({
44806
+ const missingDatabaseError = async () => {
44807
+ throw new Error("database is required");
44808
+ };
44809
+ /** The default until the config names a database: every read and write refuses. */
44810
+ const missingDatabase = {
44877
44811
  name: "missingDatabase",
44878
- models: {
44879
- bundles: {
44880
- findById: async () => {
44881
- throw new Error("database plugin is required");
44882
- },
44883
- findMany: async () => {
44884
- throw new Error("database plugin is required");
44885
- },
44886
- count: async () => {
44887
- throw new Error("database plugin is required");
44888
- }
44889
- },
44890
- bundlePatches: { findByBundleIds: async () => {
44891
- throw new Error("database plugin is required");
44892
- } },
44893
- releases: {
44894
- findById: async () => {
44895
- throw new Error("database plugin is required");
44896
- },
44897
- findMany: async () => {
44898
- throw new Error("database plugin is required");
44899
- },
44900
- findManyByScope: async () => {
44901
- throw new Error("database plugin is required");
44902
- }
44903
- },
44904
- releaseCatalogs: {
44905
- findByScopeKey: async () => {
44906
- throw new Error("database plugin is required");
44907
- },
44908
- findMany: async () => {
44909
- throw new Error("database plugin is required");
44910
- }
44911
- },
44912
- channels: {
44913
- insert: async () => {
44914
- throw new Error("database plugin is required");
44915
- },
44916
- list: async () => {
44917
- throw new Error("database plugin is required");
44918
- },
44919
- delete: async () => {
44920
- throw new Error("database plugin is required");
44921
- }
44922
- },
44923
- insights: {
44924
- recordEvent: async () => {
44925
- throw new Error("database plugin is required");
44926
- },
44927
- listEvents: async () => {
44928
- throw new Error("database plugin is required");
44929
- },
44930
- findLatestEvents: async () => {
44931
- throw new Error("database plugin is required");
44932
- },
44933
- countLatestEvents: async () => {
44934
- throw new Error("database plugin is required");
44935
- },
44936
- countEvents: async () => {
44937
- throw new Error("database plugin is required");
44938
- }
44939
- },
44940
- apiKeys: {
44941
- create: async () => {
44942
- throw new Error("database plugin is required");
44943
- },
44944
- findByHash: async () => {
44945
- throw new Error("database plugin is required");
44946
- },
44947
- list: async () => {
44948
- throw new Error("database plugin is required");
44949
- },
44950
- revoke: async () => {
44951
- throw new Error("database plugin is required");
44952
- }
44953
- }
44954
- },
44955
- commit: async () => {
44956
- throw new Error("database plugin is required");
44812
+ adapter: {
44813
+ id: "missing",
44814
+ fits: () => true,
44815
+ get: missingDatabaseError,
44816
+ query: missingDatabaseError,
44817
+ write: missingDatabaseError
44957
44818
  }
44958
- });
44819
+ };
44959
44820
  const missingStorageError = async () => {
44960
44821
  throw new Error("storage plugin is required");
44961
44822
  };
@@ -45003,7 +44864,6 @@ const getDefaultConfig = () => {
45003
44864
  return {
45004
44865
  cacheDir: path.join("node_modules", ".hot-updater"),
45005
44866
  updateStrategy: "appVersion",
45006
- compressStrategy: "zip",
45007
44867
  fingerprint: {},
45008
44868
  patch: {
45009
44869
  enabled: true,
@@ -45059,6 +44919,7 @@ const getConfigLoaderOptions = (options) => {
45059
44919
  const loadConfig = async (options) => {
45060
44920
  const { config } = await loadConfig$1(getConfigLoaderOptions(options));
45061
44921
  for (const key of ["authorityId", "catalogId"]) if (config && Object.hasOwn(config, key)) throw new Error(`Remove ${key} from hot-updater.config. Catalog identity is managed internally.`);
44922
+ if (config && Object.hasOwn(config, "compressStrategy")) throw new Error("Remove compressStrategy from hot-updater.config. OTA artifacts use manifest files with per-file Brotli compression.");
45062
44923
  const mergedConfig = mergeConfigSources(config, getDefaultConfig());
45063
44924
  const signing = normalizeSigningConfig(mergedConfig.signing);
45064
44925
  return {
@@ -45216,7 +45077,7 @@ const writeStorageResponseFile = async (response, filePath) => {
45216
45077
  return;
45217
45078
  }
45218
45079
  try {
45219
- await pipeline$1(Readable$1.fromWeb(response.body), createWriteStream$1(filePath));
45080
+ await pipeline(Readable$1.fromWeb(response.body), createWriteStream(filePath));
45220
45081
  } catch (error) {
45221
45082
  await fs$2.rm(filePath, { force: true });
45222
45083
  throw error;
@@ -45252,11 +45113,6 @@ function verifySignedFileHash({ actualFileHash, publicKey, signedFileHash }) {
45252
45113
  return false;
45253
45114
  }
45254
45115
  }
45255
- function getArchiveFilename(storageUri) {
45256
- const protocol = new URL(storageUri).protocol.replace(":", "");
45257
- const { key } = parseStorageUri(storageUri, protocol);
45258
- return path$1.posix.basename(key) || "bundle.zip";
45259
- }
45260
45116
  const getRelativeStorageDir = (relativePath) => {
45261
45117
  const normalized = relativePath.replace(/\\/g, "/");
45262
45118
  const dirname = path$1.posix.dirname(normalized);
@@ -45273,7 +45129,7 @@ async function prepareManifestAssetUploadFile({ assetPath, sourcePath, workDir }
45273
45129
  if (getManifestAssetDownloadPath(assetPath) === assetPath) return sourcePath;
45274
45130
  const uploadPath = resolvePreparedUploadPath(workDir, assetPath);
45275
45131
  await fs$2.mkdir(path$1.dirname(uploadPath), { recursive: true });
45276
- await pipeline$1(createReadStream(sourcePath), createBrotliCompress$1({ params: { [constants$2.BROTLI_PARAM_QUALITY]: 11 } }), createWriteStream$1(uploadPath));
45132
+ await pipeline(createReadStream(sourcePath), createBrotliCompress({ params: { [constants$1.BROTLI_PARAM_QUALITY]: 11 } }), createWriteStream(uploadPath));
45277
45133
  return uploadPath;
45278
45134
  }
45279
45135
  async function prepareContentAddressedUploadFile({ sourcePath, storagePath, workDir }) {
@@ -45290,9 +45146,10 @@ async function prepareManifestAssetUploadTargets({ extractDir, manifest, workDir
45290
45146
  for (const assetPath of assetPaths) {
45291
45147
  const asset = manifest.assets?.[assetPath];
45292
45148
  if (!asset?.fileHash) throw new Error(`Manifest file hash not found for ${assetPath}`);
45149
+ const sourcePath = resolveExtractedPath(extractDir, assetPath);
45293
45150
  const uploadSourcePath = await prepareManifestAssetUploadFile({
45294
45151
  assetPath,
45295
- sourcePath: resolveExtractedPath(extractDir, assetPath),
45152
+ sourcePath,
45296
45153
  workDir
45297
45154
  });
45298
45155
  const downloadByteSize = await getStorageFileByteSize(uploadSourcePath);
@@ -45301,6 +45158,7 @@ async function prepareManifestAssetUploadTargets({ extractDir, manifest, workDir
45301
45158
  if (downloadFileHash !== void 0 && !isContentAddressedAssetFileHash(downloadFileHash)) throw new Error(`Prepared asset hash must be a lowercase SHA-256 hash: ${assetPath}`);
45302
45159
  const nextAsset = {
45303
45160
  ...asset,
45161
+ byteSize: (await fs$2.stat(sourcePath)).size,
45304
45162
  downloadByteSize
45305
45163
  };
45306
45164
  delete nextAsset.downloadFileHash;
@@ -45330,143 +45188,81 @@ function resolveExtractedPath(rootDir, entryName) {
45330
45188
  const normalizedEntryName = entryName.replaceAll("\\", "/");
45331
45189
  const entryPath = path$1.resolve(rootDir, normalizedEntryName);
45332
45190
  const relativePath = path$1.relative(rootDir, entryPath);
45333
- if (relativePath.startsWith("..") || path$1.isAbsolute(relativePath) || normalizedEntryName.startsWith("/")) throw new Error(`Invalid archive entry path: ${entryName}`);
45191
+ if (relativePath.startsWith("..") || path$1.isAbsolute(relativePath) || normalizedEntryName.startsWith("/")) throw new Error(`Invalid manifest asset path: ${entryName}`);
45334
45192
  return entryPath;
45335
45193
  }
45336
- async function downloadArchive(storageUri, storagePlugin, archivePath) {
45194
+ async function downloadStorageObject(storageUri, storagePlugin, outputPath) {
45337
45195
  const protocol = new URL(storageUri).protocol.replace(":", "");
45338
45196
  if (storagePlugin?.protocol === protocol) {
45339
- await writeStorageFile(storagePlugin, storageUri, archivePath);
45197
+ await writeStorageFile(storagePlugin, storageUri, outputPath);
45340
45198
  return;
45341
45199
  }
45342
45200
  if (protocol === "http" || protocol === "https") {
45343
- await downloadFromUrl(storageUri, archivePath);
45201
+ await downloadFromUrl(storageUri, outputPath);
45344
45202
  return;
45345
45203
  }
45346
45204
  throw new Error(`No storage plugin for protocol: ${protocol}`);
45347
45205
  }
45348
45206
  async function downloadFromUrl(fileUrl, filePath) {
45349
45207
  const response = await fetch(fileUrl);
45350
- if (!response.ok) throw new Error(`Failed to download bundle archive: ${response.statusText}`);
45208
+ if (!response.ok) throw new Error(`Failed to download storage object: ${response.statusText}`);
45351
45209
  await writeStorageResponseFile(response, filePath);
45352
45210
  }
45353
- async function extractZipArchive(archivePath, extractDir) {
45354
- const zip = await import_lib.default.loadAsync(await fs$2.readFile(archivePath));
45355
- const entries = Object.values(zip.files).sort((left, right) => left.name.localeCompare(right.name));
45356
- for (const entry of entries) {
45357
- const outputPath = resolveExtractedPath(extractDir, entry.name);
45358
- if (entry.dir) {
45359
- await fs$2.mkdir(outputPath, { recursive: true });
45360
- continue;
45211
+ async function downloadManifestAssets({ bundle, manifest, outputDir, storagePlugin, workDir }) {
45212
+ await runWithConcurrency(Object.keys(manifest.assets ?? {}).sort((left, right) => left.localeCompare(right)), PROMOTE_ASSET_CONCURRENCY, async (assetPath) => {
45213
+ const asset = manifest.assets?.[assetPath];
45214
+ if (!asset?.fileHash) throw new Error(`Manifest file hash not found for ${assetPath}`);
45215
+ const downloadPath = getManifestAssetDownloadPath(assetPath);
45216
+ const storageUri = resolveManifestAssetStorageUri({
45217
+ assetBaseStorageUri: bundle.assetBaseStorageUri,
45218
+ assetPath: downloadPath,
45219
+ downloadFileHash: asset.downloadFileHash,
45220
+ fileHash: asset.fileHash
45221
+ });
45222
+ const transferPath = resolveExtractedPath(workDir, `downloads/${downloadPath}`);
45223
+ await fs$2.mkdir(path$1.dirname(transferPath), { recursive: true });
45224
+ await downloadStorageObject(storageUri, storagePlugin, transferPath);
45225
+ if (asset.downloadFileHash) {
45226
+ if (await getFileHash(transferPath) !== asset.downloadFileHash.toLowerCase()) throw new Error(`Manifest download hash mismatch for ${assetPath}`);
45361
45227
  }
45228
+ const outputPath = resolveExtractedPath(outputDir, assetPath);
45362
45229
  await fs$2.mkdir(path$1.dirname(outputPath), { recursive: true });
45363
- await fs$2.writeFile(outputPath, await entry.async("nodebuffer"));
45364
- }
45365
- }
45366
- async function extractTarBrArchive(archivePath, extractDir) {
45367
- const tarPath = path$1.join(extractDir, "bundle.tar");
45368
- const compressedBuffer = await fs$2.readFile(archivePath);
45369
- const tarBuffer = brotliDecompressSync(compressedBuffer);
45370
- await fs$2.writeFile(tarPath, tarBuffer);
45371
- try {
45372
- await uo({
45373
- file: tarPath,
45374
- cwd: extractDir,
45375
- gzip: false,
45376
- strict: true
45377
- });
45378
- } finally {
45379
- await fs$2.rm(tarPath, { force: true });
45380
- }
45381
- }
45382
- async function extractArchive(archivePath, extractDir) {
45383
- const { format } = detectCompressionFormat(path$1.basename(archivePath));
45384
- switch (format) {
45385
- case "zip":
45386
- await extractZipArchive(archivePath, extractDir);
45387
- return format;
45388
- case "tar.gz":
45389
- await uo({
45390
- file: archivePath,
45391
- cwd: extractDir,
45392
- gzip: true,
45393
- strict: true
45394
- });
45395
- return format;
45396
- case "tar.br":
45397
- await extractTarBrArchive(archivePath, extractDir);
45398
- return format;
45399
- }
45400
- }
45401
- async function getArchiveTargetFiles(bundleDir) {
45402
- const entries = await fs$2.readdir(bundleDir, { withFileTypes: true });
45403
- entries.sort((left, right) => left.name.localeCompare(right.name));
45404
- return entries.map((entry) => ({
45405
- path: path$1.join(bundleDir, entry.name),
45406
- name: entry.name
45407
- }));
45408
- }
45409
- async function createArchiveFromDirectory(sourceDir, archivePath, format) {
45410
- const targetFiles = await getArchiveTargetFiles(sourceDir);
45411
- switch (format) {
45412
- case "zip":
45413
- await createZipTargetFiles({
45414
- outfile: archivePath,
45415
- targetFiles
45416
- });
45417
- return;
45418
- case "tar.gz":
45419
- await createTarGzTargetFiles({
45420
- outfile: archivePath,
45421
- targetFiles
45422
- });
45423
- return;
45424
- case "tar.br":
45425
- await createTarBrTargetFiles({
45426
- outfile: archivePath,
45427
- targetFiles
45428
- });
45429
- return;
45430
- }
45431
- }
45432
- async function readCopiedBundleManifest(extractDir, nextBundleId) {
45433
- const manifestPath = path$1.join(extractDir, "manifest.json");
45434
- try {
45435
- await fs$2.access(manifestPath);
45436
- } catch {
45437
- throw new Error(LEGACY_BUNDLE_ERROR);
45438
- }
45439
- const manifest = JSON.parse(await fs$2.readFile(manifestPath, "utf8"));
45440
- manifest.bundleId = nextBundleId;
45441
- return {
45442
- manifest,
45443
- manifestPath
45444
- };
45230
+ if (downloadPath === assetPath) await fs$2.copyFile(transferPath, outputPath);
45231
+ else await fs$2.writeFile(outputPath, brotliDecompressSync(await fs$2.readFile(transferPath)));
45232
+ if (await getFileHash(outputPath) !== asset.fileHash.toLowerCase()) throw new Error(`Manifest file hash mismatch for ${assetPath}`);
45233
+ });
45445
45234
  }
45446
- async function createCopiedBundleArchive({ bundle, config, nextBundleId, storagePlugin }) {
45447
- const archiveFilename = getArchiveFilename(bundle.storageUri);
45235
+ async function createCopiedBundleArtifacts({ bundle, config, nextBundleId, storagePlugin }) {
45448
45236
  const workDir = await fs$2.mkdtemp(path$1.join(os.tmpdir(), "hot-updater-console-promote-"));
45449
- const sourceArchivePath = path$1.join(workDir, archiveFilename);
45450
45237
  const extractDir = path$1.join(workDir, "bundle");
45451
- const outputArchivePath = path$1.join(workDir, archiveFilename);
45238
+ const sourceManifestPath = path$1.join(workDir, "source-manifest.json");
45239
+ const manifestPath = path$1.join(extractDir, "manifest.json");
45452
45240
  const uploadedStorageUris = [];
45453
45241
  await fs$2.mkdir(extractDir, { recursive: true });
45454
45242
  try {
45455
- await downloadArchive(bundle.storageUri, storagePlugin, sourceArchivePath);
45456
- const actualSourceFileHash = await getFileHash(sourceArchivePath);
45243
+ await downloadStorageObject(bundle.manifestStorageUri, storagePlugin, sourceManifestPath);
45244
+ const actualManifestHash = await getFileHash(sourceManifestPath);
45457
45245
  const signingSession = await prepareBundleSigning(config.signing);
45458
- if (isSignedFileHash(bundle.fileHash)) {
45246
+ if (isSignedFileHash(bundle.manifestFileHash)) {
45459
45247
  if (!signingSession) throw new Error("Cannot copy a signed bundle without enabled bundle signing configuration.");
45460
45248
  if (!verifySignedFileHash({
45461
- actualFileHash: actualSourceFileHash,
45249
+ actualFileHash: actualManifestHash,
45462
45250
  publicKey: signingSession.publicKey,
45463
- signedFileHash: bundle.fileHash
45464
- })) throw new Error("Source bundle signature verification failed.");
45465
- } else if (actualSourceFileHash !== bundle.fileHash.toLowerCase()) throw new Error("Source bundle file hash verification failed.");
45466
- const format = await extractArchive(sourceArchivePath, extractDir);
45467
- const { manifest, manifestPath } = await readCopiedBundleManifest(extractDir, nextBundleId);
45251
+ signedFileHash: bundle.manifestFileHash
45252
+ })) throw new Error("Source manifest signature verification failed.");
45253
+ } else if (actualManifestHash !== bundle.manifestFileHash.toLowerCase()) throw new Error("Source manifest file hash verification failed.");
45254
+ const manifest = JSON.parse(await fs$2.readFile(sourceManifestPath, "utf8"));
45255
+ if (!manifest.assets || typeof manifest.assets !== "object") throw new Error(LEGACY_BUNDLE_ERROR);
45256
+ await downloadManifestAssets({
45257
+ bundle,
45258
+ manifest,
45259
+ outputDir: extractDir,
45260
+ storagePlugin,
45261
+ workDir
45262
+ });
45263
+ manifest.bundleId = nextBundleId;
45468
45264
  const assetPaths = Object.keys(manifest.assets ?? {}).sort((left, right) => left.localeCompare(right));
45469
- const sourceIsSigned = [bundle.fileHash, getManifestFileHash(bundle)].filter((hash) => Boolean(hash)).some((hash) => isSignedFileHash(hash));
45265
+ const sourceIsSigned = isSignedFileHash(getManifestFileHash(bundle));
45470
45266
  const manifestHasSignatures = assetPaths.some((assetPath) => Boolean(manifest.assets?.[assetPath]?.signature));
45471
45267
  if (!signingSession && (sourceIsSigned || manifestHasSignatures)) throw new Error("Cannot copy a signed bundle without enabled bundle signing configuration.");
45472
45268
  if (signingSession) {
@@ -45486,16 +45282,22 @@ async function createCopiedBundleArchive({ bundle, config, nextBundleId, storage
45486
45282
  manifest,
45487
45283
  workDir
45488
45284
  });
45285
+ const archivePath = path$1.join(workDir, "bundle.tar.br");
45286
+ manifest.archive = await createTarBrTargetFiles({
45287
+ outfile: archivePath,
45288
+ targetFiles: assetPaths.map((name) => ({
45289
+ name,
45290
+ path: resolveExtractedPath(extractDir, name)
45291
+ }))
45292
+ });
45489
45293
  await fs$2.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
45490
- await fs$2.rm(sourceArchivePath, { force: true });
45491
- await createArchiveFromDirectory(extractDir, outputArchivePath, format);
45492
- const fileHash = await getFileHash(outputArchivePath);
45493
45294
  const manifestHash = await getFileHash(manifestPath);
45494
- const nextFileHash = signingSession ? `${SIGNED_HASH_PREFIX}${await signingSession.signFileHash(fileHash)}` : fileHash;
45495
45295
  const nextManifestFileHash = signingSession ? `${SIGNED_HASH_PREFIX}${await signingSession.signFileHash(manifestHash)}` : manifestHash;
45496
- const archiveUpload = await putStorageFile(storagePlugin, createBundleStorageKey(nextBundleId), outputArchivePath);
45296
+ const archiveUpload = await putStorageFile(storagePlugin, createBundleStorageKey(nextBundleId), archivePath);
45497
45297
  uploadedStorageUris.push(archiveUpload.storageUri);
45498
- const assetBaseStorageUri = createStorageRootUriWithPath(archiveUpload.storageUri, nextBundleId, "assets");
45298
+ const manifestUpload = await putStorageFile(storagePlugin, createBundleStorageKey(nextBundleId), manifestPath);
45299
+ uploadedStorageUris.push(manifestUpload.storageUri);
45300
+ const assetBaseStorageUri = createStorageRootUriWithPath(manifestUpload.storageUri, nextBundleId, "assets");
45499
45301
  for (const assetUploadTarget of assetUploadTargets) {
45500
45302
  const storageUri = resolveManifestAssetStorageUri({
45501
45303
  assetBaseStorageUri,
@@ -45506,15 +45308,10 @@ async function createCopiedBundleArchive({ bundle, config, nextBundleId, storage
45506
45308
  const { exists } = await storagePlugin.exists({ storageUri });
45507
45309
  if (!exists) await putStorageFile(storagePlugin, getRelativeStorageDir(assetUploadTarget.storagePath) ? `assets/${getRelativeStorageDir(assetUploadTarget.storagePath)}` : "assets", assetUploadTarget.uploadSourcePath);
45508
45310
  }
45509
- const manifestUpload = await putStorageFile(storagePlugin, createBundleStorageKey(nextBundleId), manifestPath);
45510
- uploadedStorageUris.push(manifestUpload.storageUri);
45511
45311
  return {
45512
45312
  bundle: {
45513
45313
  ...bundle,
45514
45314
  id: nextBundleId,
45515
- archiveByteSize: archiveUpload.byteSize,
45516
- storageUri: archiveUpload.storageUri,
45517
- fileHash: nextFileHash,
45518
45315
  metadata: stripBundleArtifactMetadata(bundle.metadata),
45519
45316
  assetBaseStorageUri,
45520
45317
  patches: [],
@@ -45611,4 +45408,4 @@ function transformTemplate(templateString, values) {
45611
45408
  return result;
45612
45409
  }
45613
45410
  //#endregion
45614
- export { BuildLogger, ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, InitEnvFileError, InitError, LEGACY_BUNDLE_ERROR, LegacyInfrastructureError, MissingInitInputsError, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, banner, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCopiedBundleArchive, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBr, createTarBrTargetFiles, createTarGz, createTarGzTargetFiles, createZip, createZipTargetFiles, decryptJson, defineInitProvider, encryptJson, ensureInstallPackages, formatApiKeyNote, getAndroidSdkPath, getBundleSigningPublicKey, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, getStorageFileByteSize, link, loadConfig, log, makeEnv, p, prepareBundleSigning, printBanner, putStorageFile, readBundleSigningPublicKeyFile, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderImportStatements, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolvePackageVersion, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig, writeStorageFile, writeStorageResponseFile };
45411
+ export { BuildLogger, ConfigBuilder, HOT_UPDATER_PLUGINS_PATH, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, InitEnvFileError, InitError, LEGACY_BUNDLE_ERROR, LegacyInfrastructureError, MissingInitInputsError, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, banner, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCopiedBundleArtifacts, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBrTargetFiles, createZip, decryptJson, defineInitProvider, encryptJson, ensureInstallPackages, formatApiKeyNote, generateHotUpdaterPlugins, getAndroidSdkPath, getBundleSigningPublicKey, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, getStorageFileByteSize, link, loadConfig, loadHotUpdaterPlugins, log, makeEnv, p, prepareBundleSigning, printBanner, putStorageFile, readBundleSigningPublicKeyFile, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderHotUpdaterPlugins, renderImportStatements, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolvePackageVersion, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig, writeHotUpdaterPlugins, writeStorageFile, writeStorageResponseFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/cli-tools",
3
- "version": "1.0.0-rc.14",
3
+ "version": "1.0.0-rc.16",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
@@ -49,8 +49,8 @@
49
49
  "oxc-parser": "0.141.0",
50
50
  "oxc-transform": "0.141.0",
51
51
  "unconfig": "7.5.0",
52
- "@hot-updater/core": "1.0.0-rc.14",
53
- "@hot-updater/plugin-core": "1.0.0-rc.14"
52
+ "@hot-updater/core": "1.0.0-rc.15",
53
+ "@hot-updater/plugin-core": "1.0.0-rc.16"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@clack/prompts": "1.7.0",
@@ -64,7 +64,7 @@
64
64
  "tar": "^7.5.16",
65
65
  "verkit": "0.4.0",
66
66
  "workspace-tools": "^0.41.7",
67
- "@hot-updater/test-utils": "1.0.0-rc.14"
67
+ "@hot-updater/test-utils": "1.0.0-rc.16"
68
68
  },
69
69
  "inlinedDependencies": {
70
70
  "@babel/code-frame": "7.29.0",
@@ -174,7 +174,7 @@
174
174
  "7.1.0"
175
175
  ],
176
176
  "strip-final-newline": "4.0.0",
177
- "tar": "7.5.1",
177
+ "tar": "7.5.16",
178
178
  "to-regex-range": "5.0.1",
179
179
  "unicorn-magic": [
180
180
  "0.1.0",