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

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 {
@@ -31,8 +32,40 @@ declare class BuildLogger {
31
32
  private showLogFileLocation;
32
33
  }
33
34
  //#endregion
34
- //#region src/apiKeyNote.d.ts
35
- declare const formatApiKeyNote: (apiKey: string) => string;
35
+ //#region src/appSetup.d.ts
36
+ /** The credential init gives an app, through the server's clientAuth plugin. */
37
+ interface AppClientCredential {
38
+ /** Its name in output, such as "API key". */
39
+ readonly label: string;
40
+ /** The request header that carries it. */
41
+ readonly header: string;
42
+ readonly value: string;
43
+ }
44
+ /** A client plugin the server's plugins ask an app to add. */
45
+ interface AppClientPlugin {
46
+ /** The module that exports it. */
47
+ readonly module: string;
48
+ /** The export, which the app calls with no arguments. */
49
+ readonly name: string;
50
+ }
51
+ interface AppSetup {
52
+ readonly credential?: AppClientCredential;
53
+ readonly clientPlugins?: readonly AppClientPlugin[];
54
+ }
55
+ /**
56
+ * The App.tsx code init prints: `HotUpdater.init` with the server's URL,
57
+ * the header that carries the client credential when the server takes one,
58
+ * and the client plugins the server's plugins ask for.
59
+ */
60
+ declare const renderAppSetup: ({ baseURL, credential, clientPlugins }: AppSetup & {
61
+ readonly baseURL: string;
62
+ }) => string;
63
+ /** Prints the App.tsx code, when the URL is known, and the app's credential. */
64
+ declare const printAppSetup: ({ baseURL, ...setup }: AppSetup & {
65
+ readonly baseURL?: string;
66
+ /** Required, so each init passes its server plugins' client plugins. */
67
+ readonly clientPlugins: readonly AppClientPlugin[];
68
+ }) => void;
36
69
  //#endregion
37
70
  //#region src/banner.d.ts
38
71
  declare const link: (url: string) => string;
@@ -163,41 +196,16 @@ declare const copyDirToTmp: (dir: string, childDirname?: string) => Promise<{
163
196
  }>;
164
197
  //#endregion
165
198
  //#region src/createTarBr.d.ts
166
- declare const createTarBrTargetFiles: ({ outfile, targetFiles }: {
167
- targetFiles: {
168
- path: string;
169
- name: string;
170
- }[];
199
+ /** Build the one supported bulk transport from the exact manifest file set. */
200
+ declare function createTarBrTargetFiles({ outfile, targetFiles }: {
171
201
  outfile: string;
172
- }) => Promise<string>;
173
- declare const createTarBr: ({ outfile, targetDir, excludeExts }: {
174
- targetDir: string;
175
- outfile: string;
176
- excludeExts?: string[];
177
- }) => Promise<string>;
178
- //#endregion
179
- //#region src/createTarGz.d.ts
180
- declare const createTarGzTargetFiles: ({ outfile, targetFiles }: {
181
202
  targetFiles: {
182
203
  path: string;
183
204
  name: string;
184
205
  }[];
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>;
206
+ }): Promise<ManifestArchive>;
192
207
  //#endregion
193
208
  //#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
209
  declare const createZip: ({ outfile, targetDir, excludeExts }: {
202
210
  targetDir: string;
203
211
  outfile: string;
@@ -295,6 +303,34 @@ declare const readHotUpdaterInitEnv: (cwd: string, envFile?: string) => Promise<
295
303
  declare const readHotUpdaterEnv: (cwd: string) => Promise<Readonly<Record<string, string>>>;
296
304
  declare const getHotUpdaterEnvValue: (env: Readonly<Record<string, string>>, key: string) => string | undefined;
297
305
  //#endregion
306
+ //#region src/hotUpdaterPlugins.d.ts
307
+ /** Where `hot-updater init` writes a managed server's plugin list. */
308
+ declare const HOT_UPDATER_PLUGINS_PATH = "hotUpdater.plugins.ts";
309
+ /**
310
+ * The plugins file for a managed provider: a re-export of the fixed list its
311
+ * package ships, which its prebuilt server runs too.
312
+ */
313
+ declare const renderHotUpdaterPlugins: (pkg: string) => string;
314
+ type WriteHotUpdaterPluginsResult = {
315
+ readonly status: "created" | "unchanged";
316
+ readonly path: string;
317
+ } | {
318
+ readonly status: "skipped";
319
+ readonly path: string;
320
+ readonly reason: string;
321
+ };
322
+ /** Writes the plugins file; an existing file that differs is kept. */
323
+ declare const writeHotUpdaterPlugins: (pkg: string, filePath?: string) => Promise<WriteHotUpdaterPluginsResult>;
324
+ /** Writes the plugins file for `pkg` and reports what happened. */
325
+ declare const generateHotUpdaterPlugins: (pkg: string, filePath?: string) => Promise<WriteHotUpdaterPluginsResult>;
326
+ /**
327
+ * The plugins a project's `hotUpdater.plugins.ts` exports, which its server
328
+ * runs; undefined when the project has no such file. The console and the CLI
329
+ * assemble them in process as the server does, for the features and commands
330
+ * the plugins add.
331
+ */
332
+ declare const loadHotUpdaterPlugins: (cwd?: string) => Promise<readonly unknown[] | undefined>;
333
+ //#endregion
298
334
  //#region src/HotUpdateDirUtil.d.ts
299
335
  declare const HotUpdateDirUtil: {
300
336
  readonly dirName: ".hot-updater";
@@ -467,7 +503,7 @@ declare const makeEnv: (newEnvVars: Record<string, EnvVarValue>, filePath?: stri
467
503
  //#region src/promoteBundle.d.ts
468
504
  type PromoteStoragePlugin = StoragePluginWith<"get" | "put" | "exists" | "delete">;
469
505
  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 }: {
506
+ declare function createCopiedBundleArtifacts({ bundle, config, nextBundleId, storagePlugin }: {
471
507
  bundle: Bundle;
472
508
  config: ConfigResponse;
473
509
  nextBundleId: string;
@@ -475,9 +511,6 @@ declare function createCopiedBundleArchive({ bundle, config, nextBundleId, stora
475
511
  }): Promise<{
476
512
  bundle: {
477
513
  id: string;
478
- archiveByteSize: number;
479
- storageUri: string;
480
- fileHash: string;
481
514
  metadata: import("@hot-updater/core").BundleMetadata | undefined;
482
515
  assetBaseStorageUri: string;
483
516
  patches: never[];
@@ -1733,4 +1766,4 @@ type TransformTemplateArgs<T extends string> = { [Key in ExtractPlaceholders<T>]
1733
1766
  */
1734
1767
  declare function transformTemplate<T extends string>(templateString: T, values: TransformTemplateArgs<T>): string;
1735
1768
  //#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 };
1769
+ export { AppClientCredential, AppClientPlugin, 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, generateHotUpdaterPlugins, getAndroidSdkPath, getBundleSigningPublicKey, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, getStorageFileByteSize, link, loadConfig, loadHotUpdaterPlugins, log, makeEnv, p, prepareBundleSigning, printAppSetup, printBanner, putStorageFile, readBundleSigningPublicKeyFile, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderAppSetup, 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
@@ -22906,8 +22905,47 @@ var BuildLogger = class {
22906
22905
  }
22907
22906
  };
22908
22907
  //#endregion
22909
- //#region src/apiKeyNote.ts
22910
- const formatApiKeyNote = (apiKey) => apiKey;
22908
+ //#region src/appSetup.ts
22909
+ /**
22910
+ * The App.tsx code init prints: `HotUpdater.init` with the server's URL,
22911
+ * the header that carries the client credential when the server takes one,
22912
+ * and the client plugins the server's plugins ask for.
22913
+ */
22914
+ const renderAppSetup = ({ baseURL, credential, clientPlugins = [] }) => [
22915
+ "// Add this to your App.tsx",
22916
+ "import { HotUpdater } from \"@hot-updater/react-native\";",
22917
+ ...clientPlugins.map(({ module, name }) => `import { ${name} } from ${JSON.stringify(module)};`),
22918
+ "",
22919
+ "function App() {",
22920
+ " return null; // Replace with your app root.",
22921
+ "}",
22922
+ "",
22923
+ "HotUpdater.init({",
22924
+ ` baseURL: ${JSON.stringify(baseURL)},`,
22925
+ ...credential === void 0 ? [] : [
22926
+ " requestHeaders: {",
22927
+ ` ${JSON.stringify(credential.header)}: ${JSON.stringify(credential.value)},`,
22928
+ " },"
22929
+ ],
22930
+ ...clientPlugins.length === 0 ? [] : [` plugins: [${clientPlugins.map(({ name }) => `${name}()`).join(", ")}],`],
22931
+ "});",
22932
+ "",
22933
+ "// Call HotUpdater.checkForUpdate({ updateStrategy: \"appVersion\" })",
22934
+ "// when your app is ready to check.",
22935
+ "export default App;"
22936
+ ].join("\n");
22937
+ /** Prints the App.tsx code, when the URL is known, and the app's credential. */
22938
+ const printAppSetup = ({ baseURL, ...setup }) => {
22939
+ if (baseURL !== void 0) p.note(renderAppSetup({
22940
+ baseURL,
22941
+ ...setup
22942
+ }));
22943
+ const { credential } = setup;
22944
+ if (credential !== void 0) {
22945
+ p.note(credential.value, credential.label);
22946
+ p.log.message(`Store this ${credential.label} separately in a secure place.`);
22947
+ }
22948
+ };
22911
22949
  //#endregion
22912
22950
  //#region ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
22913
22951
  function ansiRegex({ onlyFirst = false } = {}) {
@@ -25043,14 +25081,14 @@ var ConfigBuilder = class {
25043
25081
  intermediateCode = "";
25044
25082
  collectedImports = /* @__PURE__ */ new Map();
25045
25083
  constructor() {
25046
- this.addImport({
25047
- pkg: "dotenv",
25048
- named: ["config"]
25049
- });
25050
25084
  this.addImport({
25051
25085
  pkg: "hot-updater",
25052
25086
  named: ["defineConfig"]
25053
25087
  });
25088
+ this.addImport({
25089
+ pkg: "node:fs",
25090
+ named: ["existsSync"]
25091
+ });
25054
25092
  }
25055
25093
  addImport(info) {
25056
25094
  const pkg = info.pkg;
@@ -25127,7 +25165,9 @@ var ConfigBuilder = class {
25127
25165
  const text = `
25128
25166
  ${importStatements}
25129
25167
 
25130
- config({ path: ".env.hotupdater" });
25168
+ if (existsSync(".env.hotupdater")) {
25169
+ process.loadEnvFile(".env.hotupdater");
25170
+ }
25131
25171
 
25132
25172
  ${this.intermediateCode ? `${this.intermediateCode}\n` : ""}
25133
25173
  export default defineConfig({
@@ -28325,7 +28365,7 @@ var fo = (s, t) => {
28325
28365
  });
28326
28366
  });
28327
28367
  };
28328
- var uo = K(co, fo, (s) => new Te(s), (s) => new Xt(s), (s, t) => {
28368
+ K(co, fo, (s) => new Te(s), (s) => new Xt(s), (s, t) => {
28329
28369
  t?.length && Xi(s, t);
28330
28370
  });
28331
28371
  var mo = (s, t) => {
@@ -28439,150 +28479,47 @@ var Ro = (s) => {
28439
28479
  };
28440
28480
  //#endregion
28441
28481
  //#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 });
28482
+ /** Build the one supported bulk transport from the exact manifest file set. */
28483
+ async function createTarBrTargetFiles({ outfile, targetFiles }) {
28484
+ await fs$2.mkdir(path$1.dirname(outfile), { recursive: true });
28485
+ const staging = await fs$2.mkdtemp(path$1.join(path$1.dirname(outfile), ".tar-br-"));
28446
28486
  try {
28487
+ const names = /* @__PURE__ */ new Set();
28447
28488
  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 });
28489
+ const name = target.name;
28490
+ 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}`);
28491
+ if (!(await fs$2.lstat(target.path)).isFile()) throw new Error(`Archive asset is not a regular file: ${name}`);
28492
+ names.add(name);
28493
+ const destination = path$1.join(staging, name);
28494
+ await fs$2.mkdir(path$1.dirname(destination), { recursive: true });
28495
+ await fs$2.copyFile(target.path, destination);
28496
+ await fs$2.chmod(destination, 420);
28497
+ }
28498
+ let tarByteSize = 0;
28499
+ const countTarBytes = new Transform({ transform(chunk, _encoding, callback) {
28500
+ tarByteSize += chunk.length;
28501
+ callback(null, chunk);
28502
+ } });
28455
28503
  await pipeline(Kn({
28456
- cwd: tmpDir,
28504
+ cwd: staging,
28457
28505
  portable: true,
28458
28506
  mtime: /* @__PURE__ */ new Date(0),
28459
28507
  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;
28508
+ }, [...names].sort()), countTarBytes, createBrotliCompress({ params: { [constants$1.BROTLI_PARAM_QUALITY]: 11 } }), createWriteStream(outfile));
28509
+ const hash = createHash("sha256");
28510
+ for await (const chunk of createReadStream(outfile)) hash.update(chunk);
28511
+ return {
28512
+ downloadFileHash: hash.digest("hex"),
28513
+ downloadByteSize: (await fs$2.stat(outfile)).size,
28514
+ tarByteSize
28515
+ };
28542
28516
  } finally {
28543
- await fs$1.rm(tmpDir, {
28517
+ await fs$2.rm(staging, {
28544
28518
  recursive: true,
28545
28519
  force: true
28546
28520
  });
28547
28521
  }
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
28522
  }
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
28523
  //#endregion
28587
28524
  //#region ../../node_modules/.pnpm/process-nextick-args@2.0.1/node_modules/process-nextick-args/index.js
28588
28525
  var require_process_nextick_args = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -37664,41 +37601,6 @@ var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((export
37664
37601
  JSZip.external = require_external();
37665
37602
  module.exports = JSZip;
37666
37603
  })))(), 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
37604
  const createZip = async ({ outfile, targetDir, excludeExts = [] }) => {
37703
37605
  const zip = new import_lib.default();
37704
37606
  await fs$1.rm(outfile, { force: true });
@@ -42298,8 +42200,8 @@ const addPropertiesAsync = {
42298
42200
  },
42299
42201
  output: {
42300
42202
  ...addProperties,
42301
- fileUrl: ({ value }) => ({ stream: createWriteStream$1(value) }),
42302
- filePath: ({ value: { file, append } }) => ({ stream: createWriteStream$1(file, append ? { flags: "a" } : {}) }),
42203
+ fileUrl: ({ value }) => ({ stream: createWriteStream(value) }),
42204
+ filePath: ({ value: { file, append } }) => ({ stream: createWriteStream(file, append ? { flags: "a" } : {}) }),
42303
42205
  webStream: ({ value }) => ({ stream: Writable.fromWeb(value) }),
42304
42206
  iterable: forbiddenIfAsync,
42305
42207
  asyncIterable: forbiddenIfAsync,
@@ -44175,7 +44077,6 @@ const getReactNativeMetadatas = (cwd) => {
44175
44077
  const HOT_UPDATER_CONFIG_PATH = "hot-updater.config.ts";
44176
44078
  const CONFIG_FILE_NAME = "hot-updater.config.ts";
44177
44079
  const MANAGED_IMPORT_PACKAGES = /* @__PURE__ */ new Set([
44178
- "dotenv",
44179
44080
  "firebase-admin",
44180
44081
  "firebase-admin/app",
44181
44082
  "hot-updater",
@@ -44428,7 +44329,6 @@ const updateManagedObject = (existing, next) => {
44428
44329
  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
44330
  return appendMissingProperties(mergedText, missingPropertyTexts, existing.objectExpression.properties.length > 0);
44430
44331
  };
44431
- const isConfigCallStatement = (statement) => statement.type === "ExpressionStatement" && statement.expression.type === "CallExpression" && statement.expression.callee.type === "Identifier" && statement.expression.callee.name === "config";
44432
44332
  const getManagedHelperName = (statement) => {
44433
44333
  if (statement.type !== "VariableDeclaration") return null;
44434
44334
  const declaration = statement.declarations[0];
@@ -44436,16 +44336,20 @@ const getManagedHelperName = (statement) => {
44436
44336
  return declaration.id.name;
44437
44337
  };
44438
44338
  const rebuildImportBlock = (source, scaffold) => {
44339
+ const imports = scaffold.imports.map((info) => info.pkg === "node:fs" ? {
44340
+ ...info,
44341
+ named: info.named?.filter((name) => name !== "existsSync")
44342
+ } : info);
44439
44343
  const importDeclarations = source.program.body.filter((statement) => statement.type === "ImportDeclaration");
44440
44344
  const firstImport = importDeclarations[0];
44441
44345
  const lastImport = importDeclarations.at(-1);
44442
44346
  if (!firstImport || !lastImport) return {
44443
44347
  start: 0,
44444
44348
  end: 0,
44445
- text: `${renderImportStatements(scaffold.imports)}\n\n`
44349
+ text: `${renderImportStatements(imports)}\n\n`
44446
44350
  };
44447
44351
  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);
44352
+ const managedImportText = renderImportStatements(imports);
44449
44353
  const nextImportBlock = [...preservedImportTexts, managedImportText].filter(Boolean).join("\n");
44450
44354
  return {
44451
44355
  start: getTopLevelFullStart(source, firstImport),
@@ -44458,12 +44362,7 @@ const rebuildManagedBody = (source, exportStart, scaffold) => {
44458
44362
  const managedHelpers = new Map(scaffold.helperStatements.map((statement) => [statement.name, statement]));
44459
44363
  const emittedHelpers = /* @__PURE__ */ new Set();
44460
44364
  const bodyStatements = [];
44461
- const configStatements = [];
44462
44365
  for (const statement of statementsBeforeExport) {
44463
- if (isConfigCallStatement(statement)) {
44464
- configStatements.push(getStatementText(source, statement));
44465
- continue;
44466
- }
44467
44366
  const helperName = getManagedHelperName(statement);
44468
44367
  if (!helperName || !MANAGED_HELPER_NAMES.has(helperName)) {
44469
44368
  bodyStatements.push(getStatementText(source, statement));
@@ -44478,8 +44377,7 @@ const rebuildManagedBody = (source, exportStart, scaffold) => {
44478
44377
  }
44479
44378
  for (const helper of scaffold.helperStatements) if (!emittedHelpers.has(helper.name)) bodyStatements.push(helper.code.trim());
44480
44379
  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`;
44380
+ const managedBody = bodyText ? `\n\n${bodyText}\n\n` : "\n\n";
44483
44381
  return {
44484
44382
  start: source.program.body.filter((statement) => statement.type === "ImportDeclaration").at(-1)?.end ?? 0,
44485
44383
  end: exportStart,
@@ -44678,6 +44576,78 @@ const getHotUpdaterEnvValue = (env, key) => {
44678
44576
  return env[key]?.trim() || void 0;
44679
44577
  };
44680
44578
  //#endregion
44579
+ //#region src/hotUpdaterPlugins.ts
44580
+ /** Where `hot-updater init` writes a managed server's plugin list. */
44581
+ const HOT_UPDATER_PLUGINS_PATH = "hotUpdater.plugins.ts";
44582
+ /**
44583
+ * The plugins file for a managed provider: a re-export of the fixed list its
44584
+ * package ships, which its prebuilt server runs too.
44585
+ */
44586
+ const renderHotUpdaterPlugins = (pkg) => [
44587
+ "// Generated by `hot-updater init`. The managed server runs these plugins,",
44588
+ `// and ${pkg} fixes the list, so the server, CLI, and console agree.`,
44589
+ `export { plugins } from "${pkg}";`,
44590
+ ""
44591
+ ].join("\n");
44592
+ /** Writes the plugins file; an existing file that differs is kept. */
44593
+ const writeHotUpdaterPlugins = async (pkg, filePath = HOT_UPDATER_PLUGINS_PATH) => {
44594
+ const text = renderHotUpdaterPlugins(pkg);
44595
+ const existingText = await fs$1.readFile(filePath, "utf-8").catch((error) => {
44596
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
44597
+ throw error;
44598
+ });
44599
+ if (existingText === null) {
44600
+ await fs$1.writeFile(filePath, text, "utf-8");
44601
+ return {
44602
+ status: "created",
44603
+ path: filePath
44604
+ };
44605
+ }
44606
+ if (existingText === text) return {
44607
+ status: "unchanged",
44608
+ path: filePath
44609
+ };
44610
+ return {
44611
+ status: "skipped",
44612
+ path: filePath,
44613
+ reason: `it does not re-export the plugins ${pkg} runs; replace it with \`export { plugins } from "${pkg}";\``
44614
+ };
44615
+ };
44616
+ /** Writes the plugins file for `pkg` and reports what happened. */
44617
+ const generateHotUpdaterPlugins = async (pkg, filePath = HOT_UPDATER_PLUGINS_PATH) => {
44618
+ const result = await writeHotUpdaterPlugins(pkg, filePath);
44619
+ if (result.status === "created") p.log.success(`Generated '${filePath}' with the plugins the server runs.`);
44620
+ else if (result.status === "skipped") p.log.warn(`Kept existing '${filePath}' unchanged: ${result.reason}`);
44621
+ return result;
44622
+ };
44623
+ /**
44624
+ * The plugins a project's `hotUpdater.plugins.ts` exports, which its server
44625
+ * runs; undefined when the project has no such file. The console and the CLI
44626
+ * assemble them in process as the server does, for the features and commands
44627
+ * the plugins add.
44628
+ */
44629
+ const loadHotUpdaterPlugins = async (cwd = getCwd()) => {
44630
+ const { config, sources } = await loadConfig$1({
44631
+ cwd,
44632
+ stopAt: path.dirname(cwd),
44633
+ merge: false,
44634
+ sources: [{
44635
+ files: "hotUpdater.plugins",
44636
+ extensions: [
44637
+ "ts",
44638
+ "mts",
44639
+ "cts",
44640
+ "js",
44641
+ "mjs",
44642
+ "cjs"
44643
+ ]
44644
+ }]
44645
+ });
44646
+ if (sources.length === 0) return void 0;
44647
+ if (!Array.isArray(config?.plugins)) throw new Error(`${path.basename(sources[0])} must export plugins, an array of plugins.`);
44648
+ return config.plugins;
44649
+ };
44650
+ //#endregion
44681
44651
  //#region src/infrastructureGeneration.ts
44682
44652
  const assertInfrastructureGenerationPayload = ({ payload, provider, resource }) => {
44683
44653
  if (typeof payload !== "object" || payload === null || !("infrastructureGeneration" in payload) || payload.infrastructureGeneration !== 1) throw new LegacyInfrastructureError(provider, resource);
@@ -44873,89 +44843,20 @@ function isMergeableValue(value) {
44873
44843
  //#endregion
44874
44844
  //#region src/loadConfig.ts
44875
44845
  var import_out = /* @__PURE__ */ __toESM(require_out(), 1);
44876
- const missingDatabase = createDatabasePlugin({
44846
+ const missingDatabaseError = async () => {
44847
+ throw new Error("database is required");
44848
+ };
44849
+ /** The default until the config names a database: every read and write refuses. */
44850
+ const missingDatabase = {
44877
44851
  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");
44852
+ adapter: {
44853
+ id: "missing",
44854
+ fits: () => true,
44855
+ get: missingDatabaseError,
44856
+ query: missingDatabaseError,
44857
+ write: missingDatabaseError
44957
44858
  }
44958
- });
44859
+ };
44959
44860
  const missingStorageError = async () => {
44960
44861
  throw new Error("storage plugin is required");
44961
44862
  };
@@ -45003,7 +44904,6 @@ const getDefaultConfig = () => {
45003
44904
  return {
45004
44905
  cacheDir: path.join("node_modules", ".hot-updater"),
45005
44906
  updateStrategy: "appVersion",
45006
- compressStrategy: "zip",
45007
44907
  fingerprint: {},
45008
44908
  patch: {
45009
44909
  enabled: true,
@@ -45059,6 +44959,7 @@ const getConfigLoaderOptions = (options) => {
45059
44959
  const loadConfig = async (options) => {
45060
44960
  const { config } = await loadConfig$1(getConfigLoaderOptions(options));
45061
44961
  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.`);
44962
+ 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
44963
  const mergedConfig = mergeConfigSources(config, getDefaultConfig());
45063
44964
  const signing = normalizeSigningConfig(mergedConfig.signing);
45064
44965
  return {
@@ -45216,7 +45117,7 @@ const writeStorageResponseFile = async (response, filePath) => {
45216
45117
  return;
45217
45118
  }
45218
45119
  try {
45219
- await pipeline$1(Readable$1.fromWeb(response.body), createWriteStream$1(filePath));
45120
+ await pipeline(Readable$1.fromWeb(response.body), createWriteStream(filePath));
45220
45121
  } catch (error) {
45221
45122
  await fs$2.rm(filePath, { force: true });
45222
45123
  throw error;
@@ -45252,11 +45153,6 @@ function verifySignedFileHash({ actualFileHash, publicKey, signedFileHash }) {
45252
45153
  return false;
45253
45154
  }
45254
45155
  }
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
45156
  const getRelativeStorageDir = (relativePath) => {
45261
45157
  const normalized = relativePath.replace(/\\/g, "/");
45262
45158
  const dirname = path$1.posix.dirname(normalized);
@@ -45273,7 +45169,7 @@ async function prepareManifestAssetUploadFile({ assetPath, sourcePath, workDir }
45273
45169
  if (getManifestAssetDownloadPath(assetPath) === assetPath) return sourcePath;
45274
45170
  const uploadPath = resolvePreparedUploadPath(workDir, assetPath);
45275
45171
  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));
45172
+ await pipeline(createReadStream(sourcePath), createBrotliCompress({ params: { [constants$1.BROTLI_PARAM_QUALITY]: 11 } }), createWriteStream(uploadPath));
45277
45173
  return uploadPath;
45278
45174
  }
45279
45175
  async function prepareContentAddressedUploadFile({ sourcePath, storagePath, workDir }) {
@@ -45290,9 +45186,10 @@ async function prepareManifestAssetUploadTargets({ extractDir, manifest, workDir
45290
45186
  for (const assetPath of assetPaths) {
45291
45187
  const asset = manifest.assets?.[assetPath];
45292
45188
  if (!asset?.fileHash) throw new Error(`Manifest file hash not found for ${assetPath}`);
45189
+ const sourcePath = resolveExtractedPath(extractDir, assetPath);
45293
45190
  const uploadSourcePath = await prepareManifestAssetUploadFile({
45294
45191
  assetPath,
45295
- sourcePath: resolveExtractedPath(extractDir, assetPath),
45192
+ sourcePath,
45296
45193
  workDir
45297
45194
  });
45298
45195
  const downloadByteSize = await getStorageFileByteSize(uploadSourcePath);
@@ -45301,6 +45198,7 @@ async function prepareManifestAssetUploadTargets({ extractDir, manifest, workDir
45301
45198
  if (downloadFileHash !== void 0 && !isContentAddressedAssetFileHash(downloadFileHash)) throw new Error(`Prepared asset hash must be a lowercase SHA-256 hash: ${assetPath}`);
45302
45199
  const nextAsset = {
45303
45200
  ...asset,
45201
+ byteSize: (await fs$2.stat(sourcePath)).size,
45304
45202
  downloadByteSize
45305
45203
  };
45306
45204
  delete nextAsset.downloadFileHash;
@@ -45330,143 +45228,81 @@ function resolveExtractedPath(rootDir, entryName) {
45330
45228
  const normalizedEntryName = entryName.replaceAll("\\", "/");
45331
45229
  const entryPath = path$1.resolve(rootDir, normalizedEntryName);
45332
45230
  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}`);
45231
+ if (relativePath.startsWith("..") || path$1.isAbsolute(relativePath) || normalizedEntryName.startsWith("/")) throw new Error(`Invalid manifest asset path: ${entryName}`);
45334
45232
  return entryPath;
45335
45233
  }
45336
- async function downloadArchive(storageUri, storagePlugin, archivePath) {
45234
+ async function downloadStorageObject(storageUri, storagePlugin, outputPath) {
45337
45235
  const protocol = new URL(storageUri).protocol.replace(":", "");
45338
45236
  if (storagePlugin?.protocol === protocol) {
45339
- await writeStorageFile(storagePlugin, storageUri, archivePath);
45237
+ await writeStorageFile(storagePlugin, storageUri, outputPath);
45340
45238
  return;
45341
45239
  }
45342
45240
  if (protocol === "http" || protocol === "https") {
45343
- await downloadFromUrl(storageUri, archivePath);
45241
+ await downloadFromUrl(storageUri, outputPath);
45344
45242
  return;
45345
45243
  }
45346
45244
  throw new Error(`No storage plugin for protocol: ${protocol}`);
45347
45245
  }
45348
45246
  async function downloadFromUrl(fileUrl, filePath) {
45349
45247
  const response = await fetch(fileUrl);
45350
- if (!response.ok) throw new Error(`Failed to download bundle archive: ${response.statusText}`);
45248
+ if (!response.ok) throw new Error(`Failed to download storage object: ${response.statusText}`);
45351
45249
  await writeStorageResponseFile(response, filePath);
45352
45250
  }
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;
45251
+ async function downloadManifestAssets({ bundle, manifest, outputDir, storagePlugin, workDir }) {
45252
+ await runWithConcurrency(Object.keys(manifest.assets ?? {}).sort((left, right) => left.localeCompare(right)), PROMOTE_ASSET_CONCURRENCY, async (assetPath) => {
45253
+ const asset = manifest.assets?.[assetPath];
45254
+ if (!asset?.fileHash) throw new Error(`Manifest file hash not found for ${assetPath}`);
45255
+ const downloadPath = getManifestAssetDownloadPath(assetPath);
45256
+ const storageUri = resolveManifestAssetStorageUri({
45257
+ assetBaseStorageUri: bundle.assetBaseStorageUri,
45258
+ assetPath: downloadPath,
45259
+ downloadFileHash: asset.downloadFileHash,
45260
+ fileHash: asset.fileHash
45261
+ });
45262
+ const transferPath = resolveExtractedPath(workDir, `downloads/${downloadPath}`);
45263
+ await fs$2.mkdir(path$1.dirname(transferPath), { recursive: true });
45264
+ await downloadStorageObject(storageUri, storagePlugin, transferPath);
45265
+ if (asset.downloadFileHash) {
45266
+ if (await getFileHash(transferPath) !== asset.downloadFileHash.toLowerCase()) throw new Error(`Manifest download hash mismatch for ${assetPath}`);
45361
45267
  }
45268
+ const outputPath = resolveExtractedPath(outputDir, assetPath);
45362
45269
  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
- };
45270
+ if (downloadPath === assetPath) await fs$2.copyFile(transferPath, outputPath);
45271
+ else await fs$2.writeFile(outputPath, brotliDecompressSync(await fs$2.readFile(transferPath)));
45272
+ if (await getFileHash(outputPath) !== asset.fileHash.toLowerCase()) throw new Error(`Manifest file hash mismatch for ${assetPath}`);
45273
+ });
45445
45274
  }
45446
- async function createCopiedBundleArchive({ bundle, config, nextBundleId, storagePlugin }) {
45447
- const archiveFilename = getArchiveFilename(bundle.storageUri);
45275
+ async function createCopiedBundleArtifacts({ bundle, config, nextBundleId, storagePlugin }) {
45448
45276
  const workDir = await fs$2.mkdtemp(path$1.join(os.tmpdir(), "hot-updater-console-promote-"));
45449
- const sourceArchivePath = path$1.join(workDir, archiveFilename);
45450
45277
  const extractDir = path$1.join(workDir, "bundle");
45451
- const outputArchivePath = path$1.join(workDir, archiveFilename);
45278
+ const sourceManifestPath = path$1.join(workDir, "source-manifest.json");
45279
+ const manifestPath = path$1.join(extractDir, "manifest.json");
45452
45280
  const uploadedStorageUris = [];
45453
45281
  await fs$2.mkdir(extractDir, { recursive: true });
45454
45282
  try {
45455
- await downloadArchive(bundle.storageUri, storagePlugin, sourceArchivePath);
45456
- const actualSourceFileHash = await getFileHash(sourceArchivePath);
45283
+ await downloadStorageObject(bundle.manifestStorageUri, storagePlugin, sourceManifestPath);
45284
+ const actualManifestHash = await getFileHash(sourceManifestPath);
45457
45285
  const signingSession = await prepareBundleSigning(config.signing);
45458
- if (isSignedFileHash(bundle.fileHash)) {
45286
+ if (isSignedFileHash(bundle.manifestFileHash)) {
45459
45287
  if (!signingSession) throw new Error("Cannot copy a signed bundle without enabled bundle signing configuration.");
45460
45288
  if (!verifySignedFileHash({
45461
- actualFileHash: actualSourceFileHash,
45289
+ actualFileHash: actualManifestHash,
45462
45290
  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);
45291
+ signedFileHash: bundle.manifestFileHash
45292
+ })) throw new Error("Source manifest signature verification failed.");
45293
+ } else if (actualManifestHash !== bundle.manifestFileHash.toLowerCase()) throw new Error("Source manifest file hash verification failed.");
45294
+ const manifest = JSON.parse(await fs$2.readFile(sourceManifestPath, "utf8"));
45295
+ if (!manifest.assets || typeof manifest.assets !== "object") throw new Error(LEGACY_BUNDLE_ERROR);
45296
+ await downloadManifestAssets({
45297
+ bundle,
45298
+ manifest,
45299
+ outputDir: extractDir,
45300
+ storagePlugin,
45301
+ workDir
45302
+ });
45303
+ manifest.bundleId = nextBundleId;
45468
45304
  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));
45305
+ const sourceIsSigned = isSignedFileHash(getManifestFileHash(bundle));
45470
45306
  const manifestHasSignatures = assetPaths.some((assetPath) => Boolean(manifest.assets?.[assetPath]?.signature));
45471
45307
  if (!signingSession && (sourceIsSigned || manifestHasSignatures)) throw new Error("Cannot copy a signed bundle without enabled bundle signing configuration.");
45472
45308
  if (signingSession) {
@@ -45486,16 +45322,22 @@ async function createCopiedBundleArchive({ bundle, config, nextBundleId, storage
45486
45322
  manifest,
45487
45323
  workDir
45488
45324
  });
45325
+ const archivePath = path$1.join(workDir, "bundle.tar.br");
45326
+ manifest.archive = await createTarBrTargetFiles({
45327
+ outfile: archivePath,
45328
+ targetFiles: assetPaths.map((name) => ({
45329
+ name,
45330
+ path: resolveExtractedPath(extractDir, name)
45331
+ }))
45332
+ });
45489
45333
  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
45334
  const manifestHash = await getFileHash(manifestPath);
45494
- const nextFileHash = signingSession ? `${SIGNED_HASH_PREFIX}${await signingSession.signFileHash(fileHash)}` : fileHash;
45495
45335
  const nextManifestFileHash = signingSession ? `${SIGNED_HASH_PREFIX}${await signingSession.signFileHash(manifestHash)}` : manifestHash;
45496
- const archiveUpload = await putStorageFile(storagePlugin, createBundleStorageKey(nextBundleId), outputArchivePath);
45336
+ const archiveUpload = await putStorageFile(storagePlugin, createBundleStorageKey(nextBundleId), archivePath);
45497
45337
  uploadedStorageUris.push(archiveUpload.storageUri);
45498
- const assetBaseStorageUri = createStorageRootUriWithPath(archiveUpload.storageUri, nextBundleId, "assets");
45338
+ const manifestUpload = await putStorageFile(storagePlugin, createBundleStorageKey(nextBundleId), manifestPath);
45339
+ uploadedStorageUris.push(manifestUpload.storageUri);
45340
+ const assetBaseStorageUri = createStorageRootUriWithPath(manifestUpload.storageUri, nextBundleId, "assets");
45499
45341
  for (const assetUploadTarget of assetUploadTargets) {
45500
45342
  const storageUri = resolveManifestAssetStorageUri({
45501
45343
  assetBaseStorageUri,
@@ -45506,15 +45348,10 @@ async function createCopiedBundleArchive({ bundle, config, nextBundleId, storage
45506
45348
  const { exists } = await storagePlugin.exists({ storageUri });
45507
45349
  if (!exists) await putStorageFile(storagePlugin, getRelativeStorageDir(assetUploadTarget.storagePath) ? `assets/${getRelativeStorageDir(assetUploadTarget.storagePath)}` : "assets", assetUploadTarget.uploadSourcePath);
45508
45350
  }
45509
- const manifestUpload = await putStorageFile(storagePlugin, createBundleStorageKey(nextBundleId), manifestPath);
45510
- uploadedStorageUris.push(manifestUpload.storageUri);
45511
45351
  return {
45512
45352
  bundle: {
45513
45353
  ...bundle,
45514
45354
  id: nextBundleId,
45515
- archiveByteSize: archiveUpload.byteSize,
45516
- storageUri: archiveUpload.storageUri,
45517
- fileHash: nextFileHash,
45518
45355
  metadata: stripBundleArtifactMetadata(bundle.metadata),
45519
45356
  assetBaseStorageUri,
45520
45357
  patches: [],
@@ -45611,4 +45448,4 @@ function transformTemplate(templateString, values) {
45611
45448
  return result;
45612
45449
  }
45613
45450
  //#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 };
45451
+ 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, generateHotUpdaterPlugins, getAndroidSdkPath, getBundleSigningPublicKey, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, getStorageFileByteSize, link, loadConfig, loadHotUpdaterPlugins, log, makeEnv, p, prepareBundleSigning, printAppSetup, printBanner, putStorageFile, readBundleSigningPublicKeyFile, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderAppSetup, 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.17",
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.17"
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.17"
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",