@hot-updater/cli-tools 1.0.0-rc.2 → 1.0.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs 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,29 +14,29 @@ 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
+ import { createBundleStorageKey, createMemoryAdapter, createStorageRootUriWithPath, getContentType, getManifestAssetDownloadPath, getManifestAssetStoragePath, isContentAddressedAssetFileHash, isRemoteDatabase, resolveManifestAssetStorageUri, toolingTargetOf } from "@hot-updater/plugin-core";
20
+ import { createHotUpdater } from "@hot-updater/server";
19
21
  import os, { constants } from "node:os";
20
- import crypto$1, { randomBytes, randomUUID } from "node:crypto";
22
+ import crypto$1, { createHash, randomBytes, randomUUID } from "node:crypto";
21
23
  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
24
  import As, { Duplex, PassThrough as PassThrough$1, Readable as Readable$1, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
25
+ import { finished, pipeline } from "node:stream/promises";
26
+ import { brotliDecompressSync, constants as constants$1, createBrotliCompress } from "node:zlib";
27
+ import { EventEmitter as EventEmitter$1, addAbortListener, on, once, setMaxListeners } from "node:events";
27
28
  import { StringDecoder } from "node:string_decoder";
29
+ import * as Ms from "zlib";
30
+ import Jr from "zlib";
28
31
  import no from "node:assert";
29
32
  import { fileURLToPath } from "node:url";
30
33
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
31
34
  import { scheduler, setImmediate as setImmediate$1, setTimeout as setTimeout$1 } from "node:timers/promises";
32
35
  import { serialize } from "node:v8";
33
- import { finished, pipeline as pipeline$1 } from "node:stream/promises";
34
36
  import { Buffer as Buffer$2 } from "node:buffer";
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
+ import { Visitor, parseSync } from "oxc-parser";
37
38
  import { loadConfig as loadConfig$1 } from "unconfig";
38
- import { brotliDecompressSync, constants as constants$2, createBrotliCompress as createBrotliCompress$1 } from "node:zlib";
39
- import { getManifestFileHash, stripBundleArtifactMetadata } from "@hot-updater/core";
39
+ import { getManifestFileHash, stripBundleArtifactMetadata } from "@hot-updater/protocol";
40
40
  import { transformSync } from "oxc-transform";
41
41
  //#endregion
42
42
  //#region src/colors.ts
@@ -22906,8 +22906,99 @@ var BuildLogger = class {
22906
22906
  }
22907
22907
  };
22908
22908
  //#endregion
22909
- //#region src/apiKeyNote.ts
22910
- const formatApiKeyNote = (apiKey) => apiKey;
22909
+ //#region src/appSetup.ts
22910
+ /** The SDK, which exports `HotUpdater` and built-in client plugins such as `insights`. */
22911
+ const SDK_MODULE = "@hot-updater/react-native";
22912
+ /**
22913
+ * The app's import lines for `HotUpdater` and `clientPlugins`: one line per
22914
+ * module, the SDK's first, so a built-in plugin joins `HotUpdater`'s import.
22915
+ * The agent scaffold renders the same lines in hot-updater's
22916
+ * `commands/infra/clientAuth.ts`, which imports no package.
22917
+ */
22918
+ const renderAppImports = (clientPlugins) => {
22919
+ const namesByModule = /* @__PURE__ */ new Map([[SDK_MODULE, ["HotUpdater"]]]);
22920
+ for (const { module, name } of clientPlugins) {
22921
+ const names = namesByModule.get(module) ?? [];
22922
+ if (!names.includes(name)) names.push(name);
22923
+ namesByModule.set(module, names);
22924
+ }
22925
+ return [...namesByModule].map(([module, names]) => `import { ${names.join(", ")} } from ${JSON.stringify(module)};`);
22926
+ };
22927
+ /**
22928
+ * The App.tsx code init prints: `HotUpdater.init` with the server's URL,
22929
+ * the header that carries the client credential when the server takes one,
22930
+ * and the client plugins the server's plugins ask for.
22931
+ */
22932
+ const renderAppSetup = ({ baseURL, credential, clientPlugins = [] }) => [
22933
+ "// Add this to your App.tsx",
22934
+ ...renderAppImports(clientPlugins),
22935
+ "",
22936
+ "function App() {",
22937
+ " return null; // Replace with your app root.",
22938
+ "}",
22939
+ "",
22940
+ "HotUpdater.init({",
22941
+ ` baseURL: ${JSON.stringify(baseURL)},`,
22942
+ ...credential === void 0 ? [] : [
22943
+ " requestHeaders: {",
22944
+ ` ${JSON.stringify(credential.header)}: ${JSON.stringify(credential.value)},`,
22945
+ " },"
22946
+ ],
22947
+ ...clientPlugins.length === 0 ? [] : [` plugins: [${clientPlugins.map(({ name }) => `${name}()`).join(", ")}],`],
22948
+ "});",
22949
+ "",
22950
+ "// Call HotUpdater.checkForUpdate({ updateStrategy: \"appVersion\" })",
22951
+ "// when your app is ready to check.",
22952
+ "export default App;"
22953
+ ].join("\n");
22954
+ /** Prints the App.tsx code, when the URL is known, and the app's credential. */
22955
+ const printAppSetup = ({ baseURL, ...setup }) => {
22956
+ if (baseURL !== void 0) p.note(renderAppSetup({
22957
+ baseURL,
22958
+ ...setup
22959
+ }));
22960
+ const { credential } = setup;
22961
+ if (credential !== void 0) {
22962
+ p.note(credential.value, credential.label);
22963
+ p.log.message(`Store this ${credential.label} separately in a secure place.`);
22964
+ }
22965
+ };
22966
+ //#endregion
22967
+ //#region src/assembleServer.ts
22968
+ /** `createHotUpdater`, with public client routes unless a plugin provides clientAuth. */
22969
+ const createServer = (database, storage, plugins) => createHotUpdater({
22970
+ database,
22971
+ storage,
22972
+ plugins,
22973
+ ...plugins.some((plugin) => plugin.provides?.clientAuth) ? {} : { clientAccess: "public" }
22974
+ });
22975
+ function assembleServer({ database, storage, plugins = [] }) {
22976
+ if (isRemoteDatabase(database)) {
22977
+ const shape = createServer({
22978
+ name: "memory",
22979
+ adapter: createMemoryAdapter()
22980
+ }, [], plugins);
22981
+ return {
22982
+ database,
22983
+ core: database.core,
22984
+ storage,
22985
+ plugins: shape.plugins,
22986
+ api: void 0,
22987
+ clientPlugins: shape.clientPlugins,
22988
+ ...shape.clientAuth === void 0 ? {} : { clientAuth: shape.clientAuth }
22989
+ };
22990
+ }
22991
+ const server = createServer(database, storage === void 0 ? [] : [storage], plugins);
22992
+ return {
22993
+ database,
22994
+ core: server.core,
22995
+ storage,
22996
+ plugins: server.plugins,
22997
+ api: server.api,
22998
+ clientPlugins: server.clientPlugins,
22999
+ ...server.clientAuth === void 0 ? {} : { clientAuth: server.clientAuth }
23000
+ };
23001
+ }
22911
23002
  //#endregion
22912
23003
  //#region ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
22913
23004
  function ansiRegex({ onlyFirst = false } = {}) {
@@ -24838,7 +24929,7 @@ const loadPrivateKey = async (privateKeyPath) => {
24838
24929
  throw new Error("Failed to load the local bundle signing private key.");
24839
24930
  }
24840
24931
  };
24841
- const createLocalSigningPlugin = ({ privateKeyPath }) => {
24932
+ const createLocalSigningAdapter = ({ privateKeyPath }) => {
24842
24933
  const privateKeys = /* @__PURE__ */ new Map();
24843
24934
  const getPrivateKey = (cwd = process.cwd()) => {
24844
24935
  const resolvedPath = resolvePath$1(cwd, privateKeyPath);
@@ -24867,7 +24958,7 @@ const createLocalSigningPlugin = ({ privateKeyPath }) => {
24867
24958
  }
24868
24959
  };
24869
24960
  };
24870
- const invalidSigningConfig = () => /* @__PURE__ */ new Error("Bundle signing must be a local key config or signing plugin. Omit signing to disable it.");
24961
+ const invalidSigningConfig = () => /* @__PURE__ */ new Error("Bundle signing must be a local key config or signing adapter. Omit signing to disable it.");
24871
24962
  const normalizeSigningConfig = (signing) => {
24872
24963
  if (signing === void 0) return void 0;
24873
24964
  if (typeof signing !== "object" || signing === null) throw invalidSigningConfig();
@@ -24878,7 +24969,7 @@ const normalizeSigningConfig = (signing) => {
24878
24969
  "getPublicKey",
24879
24970
  "sign"
24880
24971
  ].some((key) => Reflect.has(signing, key));
24881
- if ((hasPrivateKeyPath || hasEnabled) && hasPluginMembers) throw new Error("Bundle signing config cannot combine local signing fields with signing plugin fields.");
24972
+ if ((hasPrivateKeyPath || hasEnabled) && hasPluginMembers) throw new Error("Bundle signing config cannot combine local signing fields with signing adapter fields.");
24882
24973
  if (!hasPluginMembers) {
24883
24974
  if (Object.keys(signing).some((key) => key !== "enabled" && key !== "privateKeyPath")) throw new Error("Local bundle signing accepts only enabled and privateKeyPath.");
24884
24975
  const enabled = Reflect.get(signing, "enabled");
@@ -24977,14 +25068,14 @@ const prepareBundleSigning = async (signing, options = {}) => {
24977
25068
  const normalized = normalizeSigningConfig(signing);
24978
25069
  if (!normalized) return null;
24979
25070
  const cwd = options.cwd ?? getCwd();
24980
- return preparePluginSigning("enabled" in normalized ? createLocalSigningPlugin(normalized) : normalized, cwd);
25071
+ return preparePluginSigning("enabled" in normalized ? createLocalSigningAdapter(normalized) : normalized, cwd);
24981
25072
  };
24982
25073
  /** Resolves the public identity of the configured bundle signer. */
24983
25074
  const getBundleSigningPublicKey = async (signing, options = {}) => {
24984
25075
  const normalized = normalizeSigningConfig(signing);
24985
25076
  if (!normalized) return null;
24986
25077
  const cwd = options.cwd ?? getCwd();
24987
- const provider = "enabled" in normalized ? createLocalSigningPlugin(normalized) : normalized;
25078
+ const provider = "enabled" in normalized ? createLocalSigningAdapter(normalized) : normalized;
24988
25079
  return exportPublicKey(await getProviderPublicKey(provider, cwd));
24989
25080
  };
24990
25081
  //#endregion
@@ -25036,47 +25127,28 @@ const renderImportStatements = (imports) => {
25036
25127
  }
25037
25128
  return importLines.join("\n");
25038
25129
  };
25130
+ /**
25131
+ * Renders the hot-updater.config.ts init writes: the build, the server's
25132
+ * storage, database, and plugins, and the deploy settings.
25133
+ */
25039
25134
  var ConfigBuilder = class {
25040
25135
  buildType = null;
25041
25136
  storageInfo = null;
25042
25137
  databaseInfo = null;
25138
+ pluginsInfo = null;
25043
25139
  intermediateCode = "";
25044
- collectedImports = /* @__PURE__ */ new Map();
25045
- constructor() {
25046
- this.addImport({
25047
- pkg: "dotenv",
25048
- named: ["config"]
25049
- });
25050
- this.addImport({
25051
- pkg: "hot-updater",
25052
- named: ["defineConfig"]
25053
- });
25054
- }
25140
+ imports = [{
25141
+ pkg: "hot-updater",
25142
+ named: ["defineConfig"]
25143
+ }, {
25144
+ pkg: "node:fs",
25145
+ named: ["existsSync"]
25146
+ }];
25147
+ /** Adds an import, such as a credentials helper's. */
25055
25148
  addImport(info) {
25056
- const pkg = info.pkg;
25057
- const existing = this.collectedImports.get(pkg);
25058
- if (existing) {
25059
- if (info.named) for (const n of info.named) existing.named.add(n);
25060
- if (info.defaultOrNamespace && !existing.defaultOrNamespace) existing.defaultOrNamespace = info.defaultOrNamespace;
25061
- if (info.sideEffect && !existing.sideEffect) existing.sideEffect = true;
25062
- } else this.collectedImports.set(pkg, {
25063
- named: new Set(info.named ?? []),
25064
- defaultOrNamespace: info.defaultOrNamespace,
25065
- sideEffect: info.sideEffect ?? false
25066
- });
25149
+ this.imports.push(info);
25067
25150
  return this;
25068
25151
  }
25069
- addImports(imports) {
25070
- for (const imp of imports) this.addImport(imp);
25071
- }
25072
- getImportInfos() {
25073
- return normalizeImportInfos(Array.from(this.collectedImports.entries()).map(([pkg, info]) => ({
25074
- pkg,
25075
- named: Array.from(info.named),
25076
- defaultOrNamespace: info.defaultOrNamespace,
25077
- sideEffect: info.sideEffect ?? false
25078
- })));
25079
- }
25080
25152
  generateBuildConfigString() {
25081
25153
  if (!this.buildType) throw new Error("Build type must be set using .setBuildType()");
25082
25154
  switch (this.buildType) {
@@ -25086,33 +25158,34 @@ var ConfigBuilder = class {
25086
25158
  default: throw new Error(`Invalid build type: ${this.buildType}`);
25087
25159
  }
25088
25160
  }
25161
+ /** Sets the build type ('bare', 'rock', or 'expo') and its import. */
25089
25162
  setBuildType(buildType) {
25090
- if (this.buildType) console.warn("Build type is being set multiple times. Overwriting previous value.");
25091
25163
  this.buildType = buildType;
25092
- this.addImport({
25164
+ this.imports.push({
25093
25165
  pkg: `@hot-updater/${buildType}`,
25094
25166
  named: [buildType]
25095
25167
  });
25096
25168
  return this;
25097
25169
  }
25170
+ /** Sets the storage the CLI uploads to, which the server lists, and its imports. */
25098
25171
  setStorage(storageConfig) {
25099
25172
  this.storageInfo = storageConfig;
25100
- this.addImports(storageConfig.imports);
25101
- if (storageConfig.imports.some((imp) => imp.pkg.includes("firebase"))) this.addImport({
25102
- pkg: "firebase-admin/app",
25103
- named: ["applicationDefault"]
25104
- });
25173
+ this.imports.push(...storageConfig.imports);
25105
25174
  return this;
25106
25175
  }
25176
+ /** Sets the server's database and its imports. */
25107
25177
  setDatabase(databaseConfig) {
25108
25178
  this.databaseInfo = databaseConfig;
25109
- this.addImports(databaseConfig.imports);
25110
- if (databaseConfig.imports.some((imp) => imp.pkg.includes("firebase"))) this.addImport({
25111
- pkg: "firebase-admin/app",
25112
- named: ["applicationDefault"]
25113
- });
25179
+ this.imports.push(...databaseConfig.imports);
25180
+ return this;
25181
+ }
25182
+ /** Sets the plugins the server runs, such as a provider package's `plugins`. */
25183
+ setPlugins(pluginsConfig) {
25184
+ this.pluginsInfo = pluginsConfig;
25185
+ this.imports.push(...pluginsConfig.imports);
25114
25186
  return this;
25115
25187
  }
25188
+ /** Sets the code between the environment loading and the config, such as a credentials helper. */
25116
25189
  setIntermediateCode(code) {
25117
25190
  this.intermediateCode = code.trim();
25118
25191
  return this;
@@ -25121,19 +25194,22 @@ var ConfigBuilder = class {
25121
25194
  if (!this.buildType) throw new Error("Build type must be set using .setBuildType()");
25122
25195
  if (!this.storageInfo) throw new Error("Storage config must be set using .setStorage()");
25123
25196
  if (!this.databaseInfo) throw new Error("Database config must be set using .setDatabase()");
25124
- const imports = this.getImportInfos();
25125
- const importStatements = renderImportStatements(imports);
25197
+ if (!this.pluginsInfo) throw new Error("Plugins config must be set using .setPlugins()");
25198
+ const imports = normalizeImportInfos(this.imports);
25126
25199
  const buildConfigString = this.generateBuildConfigString();
25200
+ const plugins = this.pluginsInfo.configString;
25127
25201
  const text = `
25128
- ${importStatements}
25202
+ ${renderImportStatements(imports)}
25129
25203
 
25130
- config({ path: ".env.hotupdater" });
25204
+ if (existsSync(".env.hotupdater")) {
25205
+ process.loadEnvFile(".env.hotupdater");
25206
+ }
25131
25207
 
25132
- ${this.intermediateCode ? `${this.intermediateCode}\n` : ""}
25133
- export default defineConfig({
25208
+ ${this.intermediateCode ? `${this.intermediateCode}\n\n` : ""}export default defineConfig({
25134
25209
  build: ${buildConfigString},
25135
25210
  storage: ${this.storageInfo.configString},
25136
25211
  database: ${this.databaseInfo.configString},
25212
+ ${plugins === "plugins" ? "plugins" : `plugins: ${plugins}`},
25137
25213
  updateStrategy: "appVersion", // or "fingerprint"
25138
25214
  });
25139
25215
  `.trim();
@@ -25142,10 +25218,12 @@ export default defineConfig({
25142
25218
  buildConfigString,
25143
25219
  storageConfigString: this.storageInfo.configString,
25144
25220
  databaseConfigString: this.databaseInfo.configString,
25221
+ pluginsConfigString: plugins,
25145
25222
  intermediateCode: this.intermediateCode,
25146
25223
  text
25147
25224
  };
25148
25225
  }
25226
+ /** hot-updater.config.ts's text. */
25149
25227
  getResult() {
25150
25228
  return this.getScaffold().text;
25151
25229
  }
@@ -28325,7 +28403,7 @@ var fo = (s, t) => {
28325
28403
  });
28326
28404
  });
28327
28405
  };
28328
- var uo = K(co, fo, (s) => new Te(s), (s) => new Xt(s), (s, t) => {
28406
+ K(co, fo, (s) => new Te(s), (s) => new Xt(s), (s, t) => {
28329
28407
  t?.length && Xi(s, t);
28330
28408
  });
28331
28409
  var mo = (s, t) => {
@@ -28439,150 +28517,47 @@ var Ro = (s) => {
28439
28517
  };
28440
28518
  //#endregion
28441
28519
  //#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 });
28520
+ /** Build the one supported bulk transport from the exact manifest file set. */
28521
+ async function createTarBrTargetFiles({ outfile, targetFiles }) {
28522
+ await fs$2.mkdir(path$1.dirname(outfile), { recursive: true });
28523
+ const staging = await fs$2.mkdtemp(path$1.join(path$1.dirname(outfile), ".tar-br-"));
28446
28524
  try {
28525
+ const names = /* @__PURE__ */ new Set();
28447
28526
  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 });
28527
+ const name = target.name;
28528
+ 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}`);
28529
+ if (!(await fs$2.lstat(target.path)).isFile()) throw new Error(`Archive asset is not a regular file: ${name}`);
28530
+ names.add(name);
28531
+ const destination = path$1.join(staging, name);
28532
+ await fs$2.mkdir(path$1.dirname(destination), { recursive: true });
28533
+ await fs$2.copyFile(target.path, destination);
28534
+ await fs$2.chmod(destination, 420);
28535
+ }
28536
+ let tarByteSize = 0;
28537
+ const countTarBytes = new Transform({ transform(chunk, _encoding, callback) {
28538
+ tarByteSize += chunk.length;
28539
+ callback(null, chunk);
28540
+ } });
28455
28541
  await pipeline(Kn({
28456
- cwd: tmpDir,
28542
+ cwd: staging,
28457
28543
  portable: true,
28458
28544
  mtime: /* @__PURE__ */ new Date(0),
28459
28545
  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;
28546
+ }, [...names].sort()), countTarBytes, createBrotliCompress({ params: { [constants$1.BROTLI_PARAM_QUALITY]: 11 } }), createWriteStream(outfile));
28547
+ const hash = createHash("sha256");
28548
+ for await (const chunk of createReadStream(outfile)) hash.update(chunk);
28549
+ return {
28550
+ downloadFileHash: hash.digest("hex"),
28551
+ downloadByteSize: (await fs$2.stat(outfile)).size,
28552
+ tarByteSize
28553
+ };
28542
28554
  } finally {
28543
- await fs$1.rm(tmpDir, {
28555
+ await fs$2.rm(staging, {
28544
28556
  recursive: true,
28545
28557
  force: true
28546
28558
  });
28547
28559
  }
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
28560
  }
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
28561
  //#endregion
28587
28562
  //#region ../../node_modules/.pnpm/process-nextick-args@2.0.1/node_modules/process-nextick-args/index.js
28588
28563
  var require_process_nextick_args = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -37664,41 +37639,6 @@ var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((export
37664
37639
  JSZip.external = require_external();
37665
37640
  module.exports = JSZip;
37666
37641
  })))(), 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
37642
  const createZip = async ({ outfile, targetDir, excludeExts = [] }) => {
37703
37643
  const zip = new import_lib.default();
37704
37644
  await fs$1.rm(outfile, { force: true });
@@ -37734,13 +37674,13 @@ const createZip = async ({ outfile, targetDir, excludeExts = [] }) => {
37734
37674
  };
37735
37675
  //#endregion
37736
37676
  //#region src/isObject.ts
37737
- const isObject$1 = (value) => {
37677
+ const isObject$2 = (value) => {
37738
37678
  return !!value && value.constructor === Object;
37739
37679
  };
37740
37680
  //#endregion
37741
37681
  //#region src/crypto.ts
37742
37682
  const encryptJson = (jsonData, secretKey) => {
37743
- if (isObject$1(jsonData) === false) throw new Error("jsonData must be an object");
37683
+ if (isObject$2(jsonData) === false) throw new Error("jsonData must be an object");
37744
37684
  const iv = crypto.randomBytes(16);
37745
37685
  const cipher = crypto.createCipheriv("aes-256-cbc", Buffer.from(secretKey, "hex"), iv);
37746
37686
  let encrypted = cipher.update(JSON.stringify(jsonData));
@@ -40943,9 +40883,9 @@ const isReadableStream = (value) => Object.prototype.toString.call(value) === "[
40943
40883
  const isWritableStream = (value) => Object.prototype.toString.call(value) === "[object WritableStream]";
40944
40884
  const isWebStream = (value) => isReadableStream(value) || isWritableStream(value);
40945
40885
  const isTransformStream = (value) => isReadableStream(value?.readable) && isWritableStream(value?.writable);
40946
- const isAsyncIterableObject = (value) => isObject(value) && typeof value[Symbol.asyncIterator] === "function";
40947
- const isIterableObject = (value) => isObject(value) && typeof value[Symbol.iterator] === "function";
40948
- const isObject = (value) => typeof value === "object" && value !== null;
40886
+ const isAsyncIterableObject = (value) => isObject$1(value) && typeof value[Symbol.asyncIterator] === "function";
40887
+ const isIterableObject = (value) => isObject$1(value) && typeof value[Symbol.iterator] === "function";
40888
+ const isObject$1 = (value) => typeof value === "object" && value !== null;
40949
40889
  const TRANSFORM_TYPES = /* @__PURE__ */ new Set([
40950
40890
  "generator",
40951
40891
  "asyncGenerator",
@@ -42298,8 +42238,8 @@ const addPropertiesAsync = {
42298
42238
  },
42299
42239
  output: {
42300
42240
  ...addProperties,
42301
- fileUrl: ({ value }) => ({ stream: createWriteStream$1(value) }),
42302
- filePath: ({ value: { file, append } }) => ({ stream: createWriteStream$1(file, append ? { flags: "a" } : {}) }),
42241
+ fileUrl: ({ value }) => ({ stream: createWriteStream(value) }),
42242
+ filePath: ({ value: { file, append } }) => ({ stream: createWriteStream(file, append ? { flags: "a" } : {}) }),
42303
42243
  webStream: ({ value }) => ({ stream: Writable.fromWeb(value) }),
42304
42244
  iterable: forbiddenIfAsync,
42305
42245
  asyncIterable: forbiddenIfAsync,
@@ -44175,7 +44115,6 @@ const getReactNativeMetadatas = (cwd) => {
44175
44115
  const HOT_UPDATER_CONFIG_PATH = "hot-updater.config.ts";
44176
44116
  const CONFIG_FILE_NAME = "hot-updater.config.ts";
44177
44117
  const MANAGED_IMPORT_PACKAGES = /* @__PURE__ */ new Set([
44178
- "dotenv",
44179
44118
  "firebase-admin",
44180
44119
  "firebase-admin/app",
44181
44120
  "hot-updater",
@@ -44200,6 +44139,9 @@ const KNOWN_BUILD_CALLEES = /* @__PURE__ */ new Set([
44200
44139
  "expo",
44201
44140
  "rock"
44202
44141
  ]);
44142
+ /** Build adapter packages, whose imports a kept build still needs. */
44143
+ const BUILD_IMPORT_PACKAGES = new Set([...KNOWN_BUILD_CALLEES].map((callee) => `@hot-updater/${callee}`));
44144
+ const importDeclarationsOf = (source) => source.program.body.filter((statement) => statement.type === "ImportDeclaration");
44203
44145
  const parseConfigSource = (text) => {
44204
44146
  const result = parseSync(CONFIG_FILE_NAME, text, {
44205
44147
  astType: "js",
@@ -44263,13 +44205,6 @@ const getObjectPropertyName = (property) => {
44263
44205
  return null;
44264
44206
  };
44265
44207
  const isDataProperty = (property) => property.type === "Property" && property.kind === "init" && !property.method && !property.shorthand;
44266
- const hasTrailingComma = (text) => {
44267
- const closeBraceIndex = text.lastIndexOf("}");
44268
- if (closeBraceIndex === -1) return false;
44269
- let index = closeBraceIndex - 1;
44270
- while (index >= 0 && /\s/.test(text[index] ?? "")) index -= 1;
44271
- return (text[index] ?? "") === ",";
44272
- };
44273
44208
  const dedentBlock = (text) => {
44274
44209
  const lines = text.replace(/\s+$/, "").split("\n");
44275
44210
  const indents = lines.filter((line) => line.trim() !== "").map((line) => line.match(/^\s*/)?.[0].length ?? 0);
@@ -44277,16 +44212,55 @@ const dedentBlock = (text) => {
44277
44212
  return lines.map((line) => line.slice(minIndent)).join("\n");
44278
44213
  };
44279
44214
  const indentBlock = (text, indent) => dedentBlock(text).split("\n").map((line) => `${indent}${line}`).join("\n");
44280
- const appendMissingProperties = (objectText, propertyTexts, hasExistingProperties) => {
44281
- if (propertyTexts.length === 0) return objectText;
44282
- const closingIndent = /\n([ \t]*)\}$/.exec(objectText)?.[1] ?? "";
44283
- const childIndent = objectText.match(/\n([ \t]+)[^\s]/)?.[1] ?? `${closingIndent} `;
44284
- const formattedProperties = propertyTexts.map((propertyText) => indentBlock(propertyText, childIndent)).join(",\n");
44285
- const closeBraceIndex = objectText.lastIndexOf("}");
44286
- if (closeBraceIndex === -1) return objectText;
44287
- const prefix = hasExistingProperties ? hasTrailingComma(objectText) ? "\n" : ",\n" : "\n";
44288
- const suffix = `,\n${closingIndent}`;
44289
- return `${objectText.slice(0, closeBraceIndex)}${prefix}${formattedProperties}${suffix}${objectText.slice(closeBraceIndex)}`;
44215
+ const lineStartOf = (text, position) => text.lastIndexOf("\n", position - 1) + 1;
44216
+ /** The indentation of the line `position` is on. */
44217
+ const lineIndentAt = (text, position) => /^[ \t]*/.exec(text.slice(lineStartOf(text, position)))?.[0] ?? "";
44218
+ /** Whether `node` is the first thing on its line. */
44219
+ const startsLine = (text, node) => /^[ \t]*$/.test(text.slice(lineStartOf(text, node.start), node.start));
44220
+ /**
44221
+ * A property's text with the indentation of its line, so that a property
44222
+ * spanning lines dedents and indents as one block.
44223
+ */
44224
+ const getPropertyBlockText = (source, property) => source.text.slice(startsLine(source.text, property) ? lineStartOf(source.text, property.start) : property.start, property.end);
44225
+ /**
44226
+ * The edits, relative to `object`'s start, that add `propertyTexts` after its
44227
+ * last property: each on a line of its own, after that property's comma and
44228
+ * whatever comment shares its line, or after it on the line of a one-line
44229
+ * object.
44230
+ */
44231
+ const appendPropertiesEdits = (source, object, propertyTexts) => {
44232
+ if (propertyTexts.length === 0) return [];
44233
+ const { text } = source;
44234
+ const closeBrace = object.end - 1;
44235
+ const relative = (position) => position - object.start;
44236
+ const closingIndent = lineIndentAt(text, closeBrace);
44237
+ const last = object.properties.at(-1);
44238
+ if (!last) {
44239
+ const childIndent = `${closingIndent} `;
44240
+ return [{
44241
+ start: relative(object.start + 1),
44242
+ end: relative(closeBrace),
44243
+ text: `\n${propertyTexts.map((propertyText) => indentBlock(propertyText, childIndent)).join(",\n")},\n${closingIndent}`
44244
+ }];
44245
+ }
44246
+ const comma = /^\s*,/.exec(text.slice(last.end, closeBrace));
44247
+ const afterLast = last.end + (comma?.[0].length ?? 0);
44248
+ const lineEnd = text.indexOf("\n", afterLast);
44249
+ if (!startsLine(text, last) || lineEnd === -1 || lineEnd > closeBrace) return [{
44250
+ start: relative(afterLast),
44251
+ end: relative(afterLast),
44252
+ text: `${comma ? " " : ", "}${propertyTexts.map((propertyText) => dedentBlock(propertyText)).join(", ")}`
44253
+ }];
44254
+ const childIndent = lineIndentAt(text, last.start);
44255
+ return [...comma ? [] : [{
44256
+ start: relative(last.end),
44257
+ end: relative(last.end),
44258
+ text: ","
44259
+ }], {
44260
+ start: relative(lineEnd),
44261
+ end: relative(lineEnd),
44262
+ text: `\n${propertyTexts.map((propertyText) => indentBlock(propertyText, childIndent)).join(",\n")},`
44263
+ }];
44290
44264
  };
44291
44265
  const mergeObjectLiteralText = (existingObject, newObject, replaceIncompatibleProperties = []) => {
44292
44266
  const existingText = getNodeText(existingObject.source, existingObject.objectExpression);
@@ -44330,14 +44304,12 @@ const mergeObjectLiteralText = (existingObject, newObject, replaceIncompatiblePr
44330
44304
  });
44331
44305
  }
44332
44306
  }
44333
- let mergedText = existingText;
44334
- for (const edit of edits.sort((left, right) => right.start - left.start)) mergedText = mergedText.slice(0, edit.start) + edit.text + mergedText.slice(edit.end);
44335
44307
  const missingPropertyTexts = newObject.objectExpression.properties.filter((property) => {
44336
44308
  if (property.type === "SpreadElement") return !existingSpreadTexts.has(getNodeText(newObject.source, property.argument).trim());
44337
44309
  const propertyName = getObjectPropertyName(property);
44338
44310
  return propertyName ? !existingPropertyNames.has(propertyName) : false;
44339
- }).map((property) => getNodeText(newObject.source, property));
44340
- return appendMissingProperties(mergedText, missingPropertyTexts, existingObject.objectExpression.properties.length > 0);
44311
+ }).map((property) => getPropertyBlockText(newObject.source, property));
44312
+ return applyTextEdits(existingText, [...edits, ...appendPropertiesEdits(existingObject.source, existingObject.objectExpression, missingPropertyTexts)]);
44341
44313
  };
44342
44314
  const buildMergedCallInitializer = (existing, next) => {
44343
44315
  const [existingArgument] = existing.callExpression.arguments;
@@ -44359,6 +44331,11 @@ const findManagedProperty = (objectExpression, propertyName) => {
44359
44331
  const property = objectExpression.properties.find((candidate) => candidate.type === "Property" && candidate.kind === "init" && !candidate.method && !candidate.shorthand && getObjectPropertyName(candidate) === propertyName);
44360
44332
  return property?.type === "Property" ? property : null;
44361
44333
  };
44334
+ /** The `plugins` property, written either way: `plugins: [...]` or `plugins`. */
44335
+ const findPluginsProperty = (objectExpression) => {
44336
+ const property = objectExpression.properties.find((candidate) => candidate.type === "Property" && candidate.kind === "init" && !candidate.method && getObjectPropertyName(candidate) === "plugins");
44337
+ return property?.type === "Property" ? property : null;
44338
+ };
44362
44339
  const getCallCallee = (expression) => {
44363
44340
  if (expression.type !== "CallExpression" || expression.callee.type !== "Identifier") return null;
44364
44341
  return expression.callee.name;
@@ -44382,11 +44359,20 @@ const mergeHelperStatement = (existingStatementText, helper) => {
44382
44359
  if (!mergedInitializer) return null;
44383
44360
  return `${existingStatement.statement.kind === "let" || existingStatement.statement.kind === "var" ? existingStatement.statement.kind : "const"} ${helper.name} = ${mergedInitializer};`;
44384
44361
  };
44362
+ /**
44363
+ * The config object with the scaffold's `build`, `storage`, `database`, and
44364
+ * `plugins`: a call to the same adapter keeps the project's arguments and
44365
+ * gains the scaffold's missing ones, and `plugins` is the scaffold's. A build
44366
+ * that is not a plain build adapter, such as `withSentry(bare())`, stays the
44367
+ * project's (`keptBuild`). Null when `build`, `storage`, or `database` is no
44368
+ * call.
44369
+ */
44385
44370
  const updateManagedObject = (existing, next) => {
44386
44371
  const objectStart = existing.objectExpression.start;
44387
44372
  const objectText = getNodeText(existing.source, existing.objectExpression);
44388
44373
  const propertyEdits = [];
44389
44374
  const missingPropertyTexts = [];
44375
+ let keptBuild = false;
44390
44376
  for (const propertyName of [
44391
44377
  "build",
44392
44378
  "storage",
@@ -44396,7 +44382,7 @@ const updateManagedObject = (existing, next) => {
44396
44382
  const nextProperty = findManagedProperty(next.objectExpression, propertyName);
44397
44383
  if (!nextProperty) continue;
44398
44384
  if (!existingProperty) {
44399
- missingPropertyTexts.push(getNodeText(next.source, nextProperty));
44385
+ missingPropertyTexts.push(getPropertyBlockText(next.source, nextProperty));
44400
44386
  continue;
44401
44387
  }
44402
44388
  const existingCallee = getCallCallee(existingProperty.value);
@@ -44404,8 +44390,11 @@ const updateManagedObject = (existing, next) => {
44404
44390
  if (!existingCallee || !nextCallee) return null;
44405
44391
  let nextInitializerText = getNodeText(next.source, nextProperty.value);
44406
44392
  if (propertyName === "build") {
44393
+ if (!KNOWN_BUILD_CALLEES.has(existingCallee)) {
44394
+ keptBuild = true;
44395
+ continue;
44396
+ }
44407
44397
  if (existingCallee === nextCallee) continue;
44408
- if (!KNOWN_BUILD_CALLEES.has(existingCallee)) return null;
44409
44398
  } else if (existingCallee === nextCallee) {
44410
44399
  if (existingProperty.value.type !== "CallExpression" || nextProperty.value.type !== "CallExpression") return null;
44411
44400
  const mergedInitializer = buildMergedCallInitializer({
@@ -44424,29 +44413,96 @@ const updateManagedObject = (existing, next) => {
44424
44413
  text: nextInitializerText
44425
44414
  });
44426
44415
  }
44427
- let mergedText = objectText;
44428
- 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
- return appendMissingProperties(mergedText, missingPropertyTexts, existing.objectExpression.properties.length > 0);
44416
+ const existingPlugins = findPluginsProperty(existing.objectExpression);
44417
+ const nextPlugins = findPluginsProperty(next.objectExpression);
44418
+ if (nextPlugins) {
44419
+ const nextPluginsText = getNodeText(next.source, nextPlugins);
44420
+ if (!existingPlugins) missingPropertyTexts.push(getPropertyBlockText(next.source, nextPlugins));
44421
+ else if (getNodeText(existing.source, existingPlugins).replace(/\s+/gu, "") !== nextPluginsText.replace(/\s+/gu, "")) propertyEdits.push({
44422
+ start: existingPlugins.start - objectStart,
44423
+ end: existingPlugins.end - objectStart,
44424
+ text: nextPluginsText
44425
+ });
44426
+ }
44427
+ return {
44428
+ text: applyTextEdits(objectText, [...propertyEdits, ...appendPropertiesEdits(existing.source, existing.objectExpression, missingPropertyTexts)]),
44429
+ keptBuild
44430
+ };
44430
44431
  };
44431
- const isConfigCallStatement = (statement) => statement.type === "ExpressionStatement" && statement.expression.type === "CallExpression" && statement.expression.callee.type === "Identifier" && statement.expression.callee.name === "config";
44432
44432
  const getManagedHelperName = (statement) => {
44433
44433
  if (statement.type !== "VariableDeclaration") return null;
44434
44434
  const declaration = statement.declarations[0];
44435
44435
  if (declaration?.id.type !== "Identifier") return null;
44436
44436
  return declaration.id.name;
44437
44437
  };
44438
- const rebuildImportBlock = (source, scaffold) => {
44439
- const importDeclarations = source.program.body.filter((statement) => statement.type === "ImportDeclaration");
44438
+ /** Whether `text` refers to `name` as an identifier. */
44439
+ const usesIdentifier = (text, name) => new RegExp(`(?<![\\w$])${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(text);
44440
+ /**
44441
+ * What a managed package's existing imports bring that the rebuilt config
44442
+ * still uses and the scaffold doesn't import, such as a project's own `cert`
44443
+ * from firebase-admin/app beside the `credential` helper init keeps. Named
44444
+ * value imports join the scaffold's import of that package; default,
44445
+ * namespace, and type imports keep a declaration of their own.
44446
+ */
44447
+ const keptManagedImports = (declarations, scaffoldImports, usedText) => {
44448
+ const bound = new Set(scaffoldImports.flatMap((info) => [...(info.named ?? []).map((name) => name.split(/\s+as\s+/).at(-1)), ...info.defaultOrNamespace === void 0 ? [] : [info.defaultOrNamespace.replace(/^\*\s+as\s+/, "")]]));
44449
+ const imports = [];
44450
+ const texts = [];
44451
+ for (const declaration of declarations) {
44452
+ const pkg = declaration.source.value;
44453
+ const named = [];
44454
+ let defaultName;
44455
+ let namespaceName;
44456
+ for (const specifier of declaration.specifiers) {
44457
+ const local = specifier.local.name;
44458
+ if (bound.has(local) || !usesIdentifier(usedText, local)) continue;
44459
+ if (specifier.type === "ImportDefaultSpecifier") defaultName = local;
44460
+ else if (specifier.type === "ImportNamespaceSpecifier") namespaceName = local;
44461
+ else {
44462
+ const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : JSON.stringify(specifier.imported.value);
44463
+ const name = imported === local ? local : `${imported} as ${local}`;
44464
+ named.push(specifier.importKind === "type" ? `type ${name}` : name);
44465
+ }
44466
+ }
44467
+ if (named.length === 0 && !defaultName && !namespaceName) continue;
44468
+ if (declaration.importKind === "type") {
44469
+ texts.push(`import type { ${named.join(", ")} } from "${pkg}";`);
44470
+ continue;
44471
+ }
44472
+ if (named.length > 0) imports.push({
44473
+ pkg,
44474
+ named
44475
+ });
44476
+ if (namespaceName) texts.push(`import * as ${namespaceName} from "${pkg}";`);
44477
+ if (defaultName) texts.push(`import ${defaultName} from "${pkg}";`);
44478
+ }
44479
+ return {
44480
+ imports,
44481
+ texts
44482
+ };
44483
+ };
44484
+ const rebuildImportBlock = (source, scaffold, { keptBuild, usedText }) => {
44485
+ const imports = scaffold.imports.filter((info) => !(keptBuild && BUILD_IMPORT_PACKAGES.has(info.pkg))).map((info) => info.pkg === "node:fs" ? {
44486
+ ...info,
44487
+ named: info.named?.filter((name) => name !== "existsSync")
44488
+ } : info);
44489
+ const importDeclarations = importDeclarationsOf(source);
44440
44490
  const firstImport = importDeclarations[0];
44441
44491
  const lastImport = importDeclarations.at(-1);
44442
44492
  if (!firstImport || !lastImport) return {
44443
44493
  start: 0,
44444
44494
  end: 0,
44445
- text: `${renderImportStatements(scaffold.imports)}\n\n`
44446
- };
44447
- 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);
44449
- const nextImportBlock = [...preservedImportTexts, managedImportText].filter(Boolean).join("\n");
44495
+ text: `${renderImportStatements(imports)}\n\n`
44496
+ };
44497
+ const isPreserved = (declaration) => !MANAGED_IMPORT_PACKAGES.has(declaration.source.value) || keptBuild && BUILD_IMPORT_PACKAGES.has(declaration.source.value);
44498
+ const preservedImportTexts = importDeclarations.filter(isPreserved).map((declaration) => source.text.slice(getTopLevelFullStart(source, declaration), declaration.end).trim());
44499
+ const kept = keptManagedImports(importDeclarations.filter((declaration) => !isPreserved(declaration)), imports, usedText);
44500
+ const managedImportText = renderImportStatements([...imports, ...kept.imports]);
44501
+ const nextImportBlock = [
44502
+ ...preservedImportTexts,
44503
+ ...kept.texts,
44504
+ managedImportText
44505
+ ].filter(Boolean).join("\n");
44450
44506
  return {
44451
44507
  start: getTopLevelFullStart(source, firstImport),
44452
44508
  end: lastImport.end,
@@ -44458,12 +44514,7 @@ const rebuildManagedBody = (source, exportStart, scaffold) => {
44458
44514
  const managedHelpers = new Map(scaffold.helperStatements.map((statement) => [statement.name, statement]));
44459
44515
  const emittedHelpers = /* @__PURE__ */ new Set();
44460
44516
  const bodyStatements = [];
44461
- const configStatements = [];
44462
44517
  for (const statement of statementsBeforeExport) {
44463
- if (isConfigCallStatement(statement)) {
44464
- configStatements.push(getStatementText(source, statement));
44465
- continue;
44466
- }
44467
44518
  const helperName = getManagedHelperName(statement);
44468
44519
  if (!helperName || !MANAGED_HELPER_NAMES.has(helperName)) {
44469
44520
  bodyStatements.push(getStatementText(source, statement));
@@ -44478,8 +44529,7 @@ const rebuildManagedBody = (source, exportStart, scaffold) => {
44478
44529
  }
44479
44530
  for (const helper of scaffold.helperStatements) if (!emittedHelpers.has(helper.name)) bodyStatements.push(helper.code.trim());
44480
44531
  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`;
44532
+ const managedBody = bodyText ? `\n\n${bodyText}\n\n` : "\n\n";
44483
44533
  return {
44484
44534
  start: source.program.body.filter((statement) => statement.type === "ImportDeclaration").at(-1)?.end ?? 0,
44485
44535
  end: exportStart,
@@ -44498,27 +44548,49 @@ const mergeHotUpdaterConfigText = (existingText, scaffold) => {
44498
44548
  const existingConfig = findDefineConfigObject(existingSource);
44499
44549
  const nextConfig = findDefineConfigObject(nextSource);
44500
44550
  if (!existingConfig || !nextConfig) return { reason: "Existing config is not a supported `export default defineConfig({ ... })` shape." };
44501
- const nextObjectText = updateManagedObject({
44551
+ const nextObject = updateManagedObject({
44502
44552
  objectExpression: existingConfig.objectExpression,
44503
44553
  source: existingSource
44504
44554
  }, {
44505
44555
  objectExpression: nextConfig.objectExpression,
44506
44556
  source: nextSource
44507
44557
  });
44508
- if (!nextObjectText) return { reason: "Existing config uses dynamic build/storage/database expressions that cannot be merged safely." };
44558
+ if (!nextObject) return { reason: "Existing config uses dynamic build/storage/database expressions that cannot be merged safely." };
44509
44559
  const exportFullStart = getTopLevelFullStart(existingSource, existingConfig.exportDeclaration);
44510
44560
  const firstTriviaContent = existingText.slice(exportFullStart, existingConfig.exportDeclaration.start).search(/\S/);
44511
44561
  const managedBodyEnd = firstTriviaContent === -1 ? existingConfig.exportDeclaration.start : exportFullStart + firstTriviaContent;
44512
44562
  const bodyEdit = rebuildManagedBody(existingSource, managedBodyEnd, scaffold);
44513
44563
  if (!bodyEdit) return { reason: "Existing helper declarations could not be merged safely." };
44564
+ const scaffoldNames = new Map(scaffold.imports.flatMap((info) => (info.named ?? []).map((name) => [name.split(/\s+as\s+/).at(-1), info.pkg])));
44565
+ for (const declaration of importDeclarationsOf(existingSource)) {
44566
+ const pkg = declaration.source.value;
44567
+ if (MANAGED_IMPORT_PACKAGES.has(pkg) && !(nextObject.keptBuild && BUILD_IMPORT_PACKAGES.has(pkg))) continue;
44568
+ for (const specifier of declaration.specifiers) {
44569
+ const scaffoldPackage = scaffoldNames.get(specifier.local.name);
44570
+ if (scaffoldPackage !== void 0 && scaffoldPackage !== pkg) return { reason: `The import of ${specifier.local.name} from "${pkg}" takes the name init imports from "${scaffoldPackage}".` };
44571
+ }
44572
+ }
44573
+ const objectEdit = {
44574
+ start: existingConfig.objectExpression.start,
44575
+ end: existingConfig.objectExpression.end,
44576
+ text: nextObject.text
44577
+ };
44578
+ const usedText = applyTextEdits(existingText, [
44579
+ objectEdit,
44580
+ bodyEdit,
44581
+ ...importDeclarationsOf(existingSource).map((declaration) => ({
44582
+ start: declaration.start,
44583
+ end: declaration.end,
44584
+ text: ""
44585
+ }))
44586
+ ]);
44514
44587
  return { text: applyTextEdits(existingText, [
44515
- {
44516
- start: existingConfig.objectExpression.start,
44517
- end: existingConfig.objectExpression.end,
44518
- text: nextObjectText
44519
- },
44588
+ objectEdit,
44520
44589
  bodyEdit,
44521
- rebuildImportBlock(existingSource, scaffold)
44590
+ rebuildImportBlock(existingSource, scaffold, {
44591
+ keptBuild: nextObject.keptBuild,
44592
+ usedText
44593
+ })
44522
44594
  ]) };
44523
44595
  };
44524
44596
  const extractCallIdentifier = (initializer) => {
@@ -44526,9 +44598,9 @@ const extractCallIdentifier = (initializer) => {
44526
44598
  if (!match) throw new Error(`Failed to extract call identifier from "${initializer}"`);
44527
44599
  return match[1];
44528
44600
  };
44529
- const createHotUpdaterConfigScaffold = ({ build, storage, database, extraImports = [], helperStatements = [], updateStrategy = "appVersion" }) => {
44601
+ const createHotUpdaterConfigScaffold = ({ build, storage, database, plugins, extraImports = [], helperStatements = [], updateStrategy = "appVersion" }) => {
44530
44602
  const intermediateCode = helperStatements.map((statement) => statement.code.trim()).filter(Boolean).join("\n\n");
44531
- const builder = new ConfigBuilder().setBuildType(build).setStorage(storage).setDatabase(database);
44603
+ const builder = new ConfigBuilder().setBuildType(build).setStorage(storage).setDatabase(database).setPlugins(plugins);
44532
44604
  for (const extraImport of extraImports) builder.addImport(extraImport);
44533
44605
  if (intermediateCode) builder.setIntermediateCode(intermediateCode);
44534
44606
  return createHotUpdaterConfigScaffoldFromBuilder(builder, {
@@ -44553,15 +44625,22 @@ const createHotUpdaterConfigScaffoldFromBuilder = (builder, { helperStatements =
44553
44625
  initializer: scaffold.databaseConfigString,
44554
44626
  callee: extractCallIdentifier(scaffold.databaseConfigString)
44555
44627
  },
44628
+ plugins: { initializer: scaffold.pluginsConfigString },
44556
44629
  helperStatements,
44557
44630
  updateStrategy: `"${updateStrategy}"`
44558
44631
  };
44559
44632
  };
44633
+ const readTextFile = (filePath) => fs$1.readFile(filePath, "utf-8").catch((error) => {
44634
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
44635
+ throw error;
44636
+ });
44637
+ /**
44638
+ * Writes hot-updater.config.ts: the scaffold's when there is none, or the
44639
+ * existing config with the scaffold's build, storage, database, plugins, and
44640
+ * helpers merged in, keeping the project's own settings.
44641
+ */
44560
44642
  const writeHotUpdaterConfig = async (scaffold, filePath = HOT_UPDATER_CONFIG_PATH) => {
44561
- const existingText = await fs$1.readFile(filePath, "utf-8").catch((error) => {
44562
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
44563
- throw error;
44564
- });
44643
+ const existingText = await readTextFile(filePath);
44565
44644
  if (existingText === null) {
44566
44645
  await fs$1.writeFile(filePath, `${scaffold.text}\n`, "utf-8");
44567
44646
  return {
@@ -44581,6 +44660,63 @@ const writeHotUpdaterConfig = async (scaffold, filePath = HOT_UPDATER_CONFIG_PAT
44581
44660
  path: filePath
44582
44661
  };
44583
44662
  };
44663
+ /**
44664
+ * What a config needs for the scaffold's storage, database, and plugins:
44665
+ * their imports, the helpers they read, and the properties themselves.
44666
+ */
44667
+ const renderServerSettings = (scaffold) => {
44668
+ const imports = scaffold.imports.filter(({ pkg }) => pkg !== "hot-updater" && pkg !== "node:fs" && !BUILD_IMPORT_PACKAGES.has(pkg));
44669
+ const plugins = scaffold.plugins.initializer;
44670
+ return [
44671
+ renderImportStatements(imports),
44672
+ ...scaffold.helperStatements.map(({ code }) => code.trim()),
44673
+ [
44674
+ ` storage: ${scaffold.storage.initializer},`,
44675
+ ` database: ${scaffold.database.initializer},`,
44676
+ ` ${plugins === "plugins" ? "plugins" : `plugins: ${plugins}`},`
44677
+ ].join("\n")
44678
+ ].filter(Boolean).join("\n\n");
44679
+ };
44680
+ /** Where an older init wrote a managed server's plugins, which the config lists now. */
44681
+ const PLUGINS_FILE_PATH = "hotUpdater.plugins.ts";
44682
+ /**
44683
+ * Whether `text` is the plugins file an older init wrote: comments and a
44684
+ * re-export of a provider package's `plugins`, which the config imports now.
44685
+ */
44686
+ const isGeneratedPluginsFile = (text) => /^export\{plugins\}from(["'])@hot-updater\/[\w-]+\1;?$/u.test(text.replace(/^\s*\/\/.*$/gmu, "").replace(/\s+/gu, ""));
44687
+ /**
44688
+ * Writes hot-updater.config.ts and says what it did. `settings` names the
44689
+ * provider in messages, such as "Supabase". A config it cannot merge is
44690
+ * kept, with what to add to it. The plugins file an older init wrote is
44691
+ * removed; one the project wrote is named, since nothing reads it.
44692
+ */
44693
+ const writeHotUpdaterFiles = async (scaffold, { cwd = process.cwd(), settings }) => {
44694
+ const config = await writeHotUpdaterConfig(scaffold, path.join(cwd, HOT_UPDATER_CONFIG_PATH));
44695
+ if (config.status === "created") p.log.success(`Generated '${HOT_UPDATER_CONFIG_PATH}' file with ${settings} settings.`);
44696
+ else if (config.status === "merged") p.log.success(`Updated '${HOT_UPDATER_CONFIG_PATH}' file with ${settings} settings.`);
44697
+ else p.log.warn([
44698
+ `Kept existing '${HOT_UPDATER_CONFIG_PATH}' unchanged: ${config.reason}`,
44699
+ `Set storage, database, and plugins in its config, as init writes them for ${settings}:`,
44700
+ "",
44701
+ renderServerSettings(scaffold)
44702
+ ].join("\n"));
44703
+ const pluginsPath = path.join(cwd, PLUGINS_FILE_PATH);
44704
+ const pluginsText = await readTextFile(pluginsPath);
44705
+ if (pluginsText === null) return { config };
44706
+ if (isGeneratedPluginsFile(pluginsText)) {
44707
+ await fs$1.rm(pluginsPath);
44708
+ p.log.success(`Removed '${PLUGINS_FILE_PATH}': \`plugins\` in '${HOT_UPDATER_CONFIG_PATH}' lists the plugins the server runs.`);
44709
+ return {
44710
+ config,
44711
+ pluginsFile: "removed"
44712
+ };
44713
+ }
44714
+ p.log.warn(`Nothing reads '${pluginsPath}' anymore: \`plugins\` in '${HOT_UPDATER_CONFIG_PATH}' lists the plugins the server runs. Delete it.`);
44715
+ return {
44716
+ config,
44717
+ pluginsFile: "kept"
44718
+ };
44719
+ };
44584
44720
  //#endregion
44585
44721
  //#region src/initOptions.ts
44586
44722
  var InitError = class extends Error {
@@ -44873,100 +45009,6 @@ function isMergeableValue(value) {
44873
45009
  //#endregion
44874
45010
  //#region src/loadConfig.ts
44875
45011
  var import_out = /* @__PURE__ */ __toESM(require_out(), 1);
44876
- const missingDatabase = createDatabasePlugin({
44877
- 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
- record: async () => {
44925
- throw new Error("database plugin is required");
44926
- },
44927
- listEvents: async () => {
44928
- throw new Error("database plugin is required");
44929
- },
44930
- findInstallations: async () => {
44931
- throw new Error("database plugin is required");
44932
- },
44933
- countInstallations: 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");
44957
- }
44958
- });
44959
- const missingStorageError = async () => {
44960
- throw new Error("storage plugin is required");
44961
- };
44962
- const missingStorage = createStoragePlugin({
44963
- name: "missingStorage",
44964
- protocol: "missing",
44965
- put: missingStorageError,
44966
- get: missingStorageError,
44967
- exists: missingStorageError,
44968
- delete: missingStorageError
44969
- });
44970
45012
  const getDefaultPlatformConfig = () => {
44971
45013
  let infoPlistPaths = [];
44972
45014
  try {
@@ -45003,7 +45045,6 @@ const getDefaultConfig = () => {
45003
45045
  return {
45004
45046
  cacheDir: path.join("node_modules", ".hot-updater"),
45005
45047
  updateStrategy: "appVersion",
45006
- compressStrategy: "zip",
45007
45048
  fingerprint: {},
45008
45049
  patch: {
45009
45050
  enabled: true,
@@ -45016,25 +45057,26 @@ const getDefaultConfig = () => {
45016
45057
  ios: {}
45017
45058
  },
45018
45059
  build: () => {
45019
- throw new Error("build plugin is required");
45060
+ throw new Error("build adapter is required");
45020
45061
  },
45021
- storage: missingStorage,
45022
- database: missingDatabase
45062
+ plugins: []
45023
45063
  };
45024
45064
  };
45025
45065
  const mergeConfigSources = (...sources) => {
45026
45066
  const mergedConfig = sources.reduceRight((mergedConfig, source) => merge(mergedConfig, source ?? {}), {});
45027
45067
  const database = sources.find((source) => source?.database)?.database;
45068
+ const plugins = sources.find((source) => source?.plugins)?.plugins;
45028
45069
  const signing = sources.find((source) => source?.signing)?.signing;
45029
45070
  const storage = sources.find((source) => source?.storage)?.storage;
45030
45071
  return {
45031
45072
  ...mergedConfig,
45032
45073
  ...database ? { database } : {},
45074
+ ...plugins ? { plugins } : {},
45033
45075
  ...signing ? { signing } : {},
45034
45076
  ...storage ? { storage } : {}
45035
45077
  };
45036
45078
  };
45037
- const getConfigLoaderOptions = (options) => {
45079
+ const getConfigLoaderOptions = () => {
45038
45080
  const cwd = getCwd();
45039
45081
  return {
45040
45082
  cwd,
@@ -45049,16 +45091,38 @@ const getConfigLoaderOptions = (options) => {
45049
45091
  "cts",
45050
45092
  "mjs",
45051
45093
  "mts"
45052
- ],
45053
- rewrite: async (config) => {
45054
- return typeof config === "function" ? config(options) : config;
45055
- }
45094
+ ]
45056
45095
  }]
45057
45096
  };
45058
45097
  };
45059
- const loadConfig = async (options) => {
45060
- const { config } = await loadConfig$1(getConfigLoaderOptions(options));
45098
+ /** Each load runs the config file again, which creates its adapters again. */
45099
+ const loadConfigFile = async () => {
45100
+ const { config } = await loadConfig$1(getConfigLoaderOptions());
45101
+ return config;
45102
+ };
45103
+ const configFor = async (source, options) => typeof source === "function" ? await source(options) : source;
45104
+ const loadConfig = async (options) => resolveConfig(await configFor(await loadConfigFile(), options));
45105
+ /**
45106
+ * hot-updater.config for each of `platforms`, with the file loaded once. A
45107
+ * config object gives every platform the same database, storage, and
45108
+ * plugins; a config function runs once per platform, so the adapters it
45109
+ * creates are that platform's own.
45110
+ */
45111
+ const loadPlatformConfigs = async (platforms, { channel }) => {
45112
+ const source = await loadConfigFile();
45113
+ const configs = [];
45114
+ for (const platform of platforms) configs.push({
45115
+ platform,
45116
+ config: resolveConfig(await configFor(source, {
45117
+ channel,
45118
+ platform
45119
+ }))
45120
+ });
45121
+ return configs;
45122
+ };
45123
+ const resolveConfig = (config) => {
45061
45124
  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.`);
45125
+ 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
45126
  const mergedConfig = mergeConfigSources(config, getDefaultConfig());
45063
45127
  const signing = normalizeSigningConfig(mergedConfig.signing);
45064
45128
  return {
@@ -45067,6 +45131,97 @@ const loadConfig = async (options) => {
45067
45131
  };
45068
45132
  };
45069
45133
  //#endregion
45134
+ //#region src/serverDefinition.ts
45135
+ /** A server definition tooling cannot use, with what to do about it. */
45136
+ var ServerDefinitionError = class extends Error {
45137
+ name = "ServerDefinitionError";
45138
+ };
45139
+ const isRecord = (value) => typeof value === "object" && value !== null;
45140
+ /** Whether `value` has the public properties tooling reads from a server definition. */
45141
+ const isServerDefinition = (value) => isRecord(value) && isRecord(value.database) && typeof value.database.name === "string" && Array.isArray(value.storage) && Array.isArray(value.plugins) && Array.isArray(value.clientPlugins) && Array.isArray(value.clientEndpoints) && isRecord(value.core) && isRecord(value.api);
45142
+ /**
45143
+ * `value` as a server definition, or a `ServerDefinitionError` that says
45144
+ * what `source`, the module it came from, must export instead.
45145
+ */
45146
+ const serverDefinitionOf = (value, source = "The server definition") => {
45147
+ if (isServerDefinition(value)) return value;
45148
+ if (isRecord(value) && "handlers" in value) throw new ServerDefinitionError(`${source} exports a hotUpdater from an older @hot-updater/server. Upgrade @hot-updater/server to the version of hot-updater.`);
45149
+ throw new ServerDefinitionError(`${source} must export hotUpdater: the server createHotUpdater({ database, storage, plugins }) returns.`);
45150
+ };
45151
+ const fail = (message) => {
45152
+ throw new ServerDefinitionError(message);
45153
+ };
45154
+ const isObject = (value) => isRecord(value) && !Array.isArray(value);
45155
+ /** The clientAuth plugin's credential contribution, checked. */
45156
+ const credentialOf = ({ plugins, clientAuth }) => {
45157
+ if (clientAuth === void 0) return void 0;
45158
+ const id = clientAuth.plugin;
45159
+ const credential = plugins.find((plugin) => plugin.id === id)?.cli?.clientCredential;
45160
+ if (!isObject(credential) || typeof credential.label !== "string" || typeof credential.header !== "string" || typeof credential.env !== "string" || typeof credential.generate !== "function" || typeof credential.provision !== "function") return fail(`Plugin "${id}" provides clientAuth but no cli.clientCredential with a label, header, env, generate, and provision, so init cannot give an app its credential.`);
45161
+ return {
45162
+ plugin: id,
45163
+ credential
45164
+ };
45165
+ };
45166
+ /**
45167
+ * The client-route policy a definition's plugins set: the plugin that
45168
+ * provides clientAuth, the headers its decision reads, and the credential an
45169
+ * app sends. Undefined when client routes are public.
45170
+ */
45171
+ const clientAuthOf = (definition) => {
45172
+ const found = credentialOf(definition);
45173
+ if (found === void 0) return void 0;
45174
+ const { label, header, env } = found.credential;
45175
+ return {
45176
+ plugin: found.plugin,
45177
+ varyHeaders: definition.clientAuth?.varyHeaders ?? [],
45178
+ credential: {
45179
+ label,
45180
+ header: header.toLowerCase(),
45181
+ env
45182
+ }
45183
+ };
45184
+ };
45185
+ /**
45186
+ * A new client credential, made without a database, so tooling can save it
45187
+ * before it registers it. Undefined when client routes are public.
45188
+ */
45189
+ const generateClientCredential = (definition) => credentialOf(definition)?.credential.generate();
45190
+ /**
45191
+ * Provisions the credential an app sends to client routes, through the
45192
+ * plugin that provides clientAuth, on the definition's database: a
45193
+ * credential saved in `env` is registered again rather than replaced.
45194
+ * Undefined when client routes are public.
45195
+ */
45196
+ const provisionClientCredential = async (definition, input) => {
45197
+ const found = credentialOf(definition);
45198
+ if (found === void 0) return void 0;
45199
+ const { label, header, env, provision } = found.credential;
45200
+ const existing = input.env[env]?.trim();
45201
+ const value = await provision(definition.api[found.plugin], {
45202
+ ...existing ? { existing } : {},
45203
+ name: input.name
45204
+ });
45205
+ return {
45206
+ label,
45207
+ header: header.toLowerCase(),
45208
+ env,
45209
+ value
45210
+ };
45211
+ };
45212
+ /** Whether `hot-updater db generate` writes schema files for a definition's database. */
45213
+ const generatesSchema = ({ database }) => database.generateSchema !== void 0;
45214
+ /** The migrator for a definition's database: core's tables and its plugins' tables. */
45215
+ const createMigrator = ({ database, plugins }) => {
45216
+ if (database.createMigrator === void 0) throw new ServerDefinitionError(database.generateSchema === void 0 ? `The ${database.name} database has no migrator; its provider applies the schema.` : `The ${database.name} database applies its schema from migration files: run \`hot-updater db generate\`, then apply the file with the provider's tooling.`);
45217
+ return database.createMigrator(toolingTargetOf(plugins));
45218
+ };
45219
+ /** The schema file `hot-updater db generate` writes for a definition's database. */
45220
+ const generateSchema = ({ database, plugins }, version, name) => {
45221
+ if (database.generateSchema === void 0) throw new ServerDefinitionError(`The ${database.name} database has no schema generator; run \`hot-updater db migrate\` instead.`);
45222
+ return database.generateSchema(version, name, toolingTargetOf(plugins));
45223
+ };
45224
+ //#endregion
45070
45225
  //#region src/log.ts
45071
45226
  const log = {
45072
45227
  normal: (message) => console.log(message),
@@ -45181,6 +45336,31 @@ const makeEnv = async (newEnvVars, filePath = ".env.hotupdater", options) => {
45181
45336
  }
45182
45337
  };
45183
45338
  //#endregion
45339
+ //#region src/moduleSpecifiers.ts
45340
+ /**
45341
+ * The modules `source` imports or re-exports, statically or with an
45342
+ * `import()` of a string literal or a template without substitutions: what a
45343
+ * runtime that loads it unbundled, such as Deno through an import map,
45344
+ * resolves. An `import()` of a computed specifier, such as
45345
+ * `import("dayjs/locale/" + name + ".js")`, names no module to resolve.
45346
+ * Minified code reads the same as formatted code. `fileName` gives the
45347
+ * language, such as `.ts`.
45348
+ */
45349
+ const moduleSpecifiersOf = (fileName, source) => {
45350
+ const { errors, module, program } = parseSync(fileName, source);
45351
+ if (errors.length > 0) throw new Error(`Could not read the imports of ${fileName}: ${errors[0].message}`);
45352
+ const dynamicImports = [];
45353
+ new Visitor({ ImportExpression({ source: specifier }) {
45354
+ if (specifier.type === "Literal" && typeof specifier.value === "string") dynamicImports.push(specifier.value);
45355
+ else if (specifier.type === "TemplateLiteral" && specifier.expressions.length === 0 && typeof specifier.quasis[0]?.value.cooked === "string") dynamicImports.push(specifier.quasis[0].value.cooked);
45356
+ } }).visit(program);
45357
+ return [.../* @__PURE__ */ new Set([
45358
+ ...module.staticImports.map(({ moduleRequest }) => moduleRequest.value),
45359
+ ...module.staticExports.flatMap(({ entries }) => entries.flatMap(({ moduleRequest }) => moduleRequest === null ? [] : [moduleRequest.value])),
45360
+ ...dynamicImports
45361
+ ])];
45362
+ };
45363
+ //#endregion
45184
45364
  //#region src/storageFiles.ts
45185
45365
  const getStorageFileByteSize = async (filePath) => {
45186
45366
  const { size } = await fs$2.stat(filePath, { bigint: true });
@@ -45216,7 +45396,7 @@ const writeStorageResponseFile = async (response, filePath) => {
45216
45396
  return;
45217
45397
  }
45218
45398
  try {
45219
- await pipeline$1(Readable$1.fromWeb(response.body), createWriteStream$1(filePath));
45399
+ await pipeline(Readable$1.fromWeb(response.body), createWriteStream(filePath));
45220
45400
  } catch (error) {
45221
45401
  await fs$2.rm(filePath, { force: true });
45222
45402
  throw error;
@@ -45252,11 +45432,6 @@ function verifySignedFileHash({ actualFileHash, publicKey, signedFileHash }) {
45252
45432
  return false;
45253
45433
  }
45254
45434
  }
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
45435
  const getRelativeStorageDir = (relativePath) => {
45261
45436
  const normalized = relativePath.replace(/\\/g, "/");
45262
45437
  const dirname = path$1.posix.dirname(normalized);
@@ -45273,7 +45448,7 @@ async function prepareManifestAssetUploadFile({ assetPath, sourcePath, workDir }
45273
45448
  if (getManifestAssetDownloadPath(assetPath) === assetPath) return sourcePath;
45274
45449
  const uploadPath = resolvePreparedUploadPath(workDir, assetPath);
45275
45450
  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));
45451
+ await pipeline(createReadStream(sourcePath), createBrotliCompress({ params: { [constants$1.BROTLI_PARAM_QUALITY]: 11 } }), createWriteStream(uploadPath));
45277
45452
  return uploadPath;
45278
45453
  }
45279
45454
  async function prepareContentAddressedUploadFile({ sourcePath, storagePath, workDir }) {
@@ -45290,9 +45465,10 @@ async function prepareManifestAssetUploadTargets({ extractDir, manifest, workDir
45290
45465
  for (const assetPath of assetPaths) {
45291
45466
  const asset = manifest.assets?.[assetPath];
45292
45467
  if (!asset?.fileHash) throw new Error(`Manifest file hash not found for ${assetPath}`);
45468
+ const sourcePath = resolveExtractedPath(extractDir, assetPath);
45293
45469
  const uploadSourcePath = await prepareManifestAssetUploadFile({
45294
45470
  assetPath,
45295
- sourcePath: resolveExtractedPath(extractDir, assetPath),
45471
+ sourcePath,
45296
45472
  workDir
45297
45473
  });
45298
45474
  const downloadByteSize = await getStorageFileByteSize(uploadSourcePath);
@@ -45301,6 +45477,7 @@ async function prepareManifestAssetUploadTargets({ extractDir, manifest, workDir
45301
45477
  if (downloadFileHash !== void 0 && !isContentAddressedAssetFileHash(downloadFileHash)) throw new Error(`Prepared asset hash must be a lowercase SHA-256 hash: ${assetPath}`);
45302
45478
  const nextAsset = {
45303
45479
  ...asset,
45480
+ byteSize: (await fs$2.stat(sourcePath)).size,
45304
45481
  downloadByteSize
45305
45482
  };
45306
45483
  delete nextAsset.downloadFileHash;
@@ -45330,143 +45507,81 @@ function resolveExtractedPath(rootDir, entryName) {
45330
45507
  const normalizedEntryName = entryName.replaceAll("\\", "/");
45331
45508
  const entryPath = path$1.resolve(rootDir, normalizedEntryName);
45332
45509
  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}`);
45510
+ if (relativePath.startsWith("..") || path$1.isAbsolute(relativePath) || normalizedEntryName.startsWith("/")) throw new Error(`Invalid manifest asset path: ${entryName}`);
45334
45511
  return entryPath;
45335
45512
  }
45336
- async function downloadArchive(storageUri, storagePlugin, archivePath) {
45513
+ async function downloadStorageObject(storageUri, storageAdapter, outputPath) {
45337
45514
  const protocol = new URL(storageUri).protocol.replace(":", "");
45338
- if (storagePlugin?.protocol === protocol) {
45339
- await writeStorageFile(storagePlugin, storageUri, archivePath);
45515
+ if (storageAdapter?.protocol === protocol) {
45516
+ await writeStorageFile(storageAdapter, storageUri, outputPath);
45340
45517
  return;
45341
45518
  }
45342
45519
  if (protocol === "http" || protocol === "https") {
45343
- await downloadFromUrl(storageUri, archivePath);
45520
+ await downloadFromUrl(storageUri, outputPath);
45344
45521
  return;
45345
45522
  }
45346
- throw new Error(`No storage plugin for protocol: ${protocol}`);
45523
+ throw new Error(`No storage adapter for protocol: ${protocol}`);
45347
45524
  }
45348
45525
  async function downloadFromUrl(fileUrl, filePath) {
45349
45526
  const response = await fetch(fileUrl);
45350
- if (!response.ok) throw new Error(`Failed to download bundle archive: ${response.statusText}`);
45527
+ if (!response.ok) throw new Error(`Failed to download storage object: ${response.statusText}`);
45351
45528
  await writeStorageResponseFile(response, filePath);
45352
45529
  }
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;
45530
+ async function downloadManifestAssets({ bundle, manifest, outputDir, storageAdapter, workDir }) {
45531
+ await runWithConcurrency(Object.keys(manifest.assets ?? {}).sort((left, right) => left.localeCompare(right)), PROMOTE_ASSET_CONCURRENCY, async (assetPath) => {
45532
+ const asset = manifest.assets?.[assetPath];
45533
+ if (!asset?.fileHash) throw new Error(`Manifest file hash not found for ${assetPath}`);
45534
+ const downloadPath = getManifestAssetDownloadPath(assetPath);
45535
+ const storageUri = resolveManifestAssetStorageUri({
45536
+ assetBaseStorageUri: bundle.assetBaseStorageUri,
45537
+ assetPath: downloadPath,
45538
+ downloadFileHash: asset.downloadFileHash,
45539
+ fileHash: asset.fileHash
45540
+ });
45541
+ const transferPath = resolveExtractedPath(workDir, `downloads/${downloadPath}`);
45542
+ await fs$2.mkdir(path$1.dirname(transferPath), { recursive: true });
45543
+ await downloadStorageObject(storageUri, storageAdapter, transferPath);
45544
+ if (asset.downloadFileHash) {
45545
+ if (await getFileHash(transferPath) !== asset.downloadFileHash.toLowerCase()) throw new Error(`Manifest download hash mismatch for ${assetPath}`);
45361
45546
  }
45547
+ const outputPath = resolveExtractedPath(outputDir, assetPath);
45362
45548
  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
- };
45549
+ if (downloadPath === assetPath) await fs$2.copyFile(transferPath, outputPath);
45550
+ else await fs$2.writeFile(outputPath, brotliDecompressSync(await fs$2.readFile(transferPath)));
45551
+ if (await getFileHash(outputPath) !== asset.fileHash.toLowerCase()) throw new Error(`Manifest file hash mismatch for ${assetPath}`);
45552
+ });
45445
45553
  }
45446
- async function createCopiedBundleArchive({ bundle, config, nextBundleId, storagePlugin }) {
45447
- const archiveFilename = getArchiveFilename(bundle.storageUri);
45554
+ async function createCopiedBundleArtifacts({ bundle, config, nextBundleId, storageAdapter }) {
45448
45555
  const workDir = await fs$2.mkdtemp(path$1.join(os.tmpdir(), "hot-updater-console-promote-"));
45449
- const sourceArchivePath = path$1.join(workDir, archiveFilename);
45450
45556
  const extractDir = path$1.join(workDir, "bundle");
45451
- const outputArchivePath = path$1.join(workDir, archiveFilename);
45557
+ const sourceManifestPath = path$1.join(workDir, "source-manifest.json");
45558
+ const manifestPath = path$1.join(extractDir, "manifest.json");
45452
45559
  const uploadedStorageUris = [];
45453
45560
  await fs$2.mkdir(extractDir, { recursive: true });
45454
45561
  try {
45455
- await downloadArchive(bundle.storageUri, storagePlugin, sourceArchivePath);
45456
- const actualSourceFileHash = await getFileHash(sourceArchivePath);
45562
+ await downloadStorageObject(bundle.manifestStorageUri, storageAdapter, sourceManifestPath);
45563
+ const actualManifestHash = await getFileHash(sourceManifestPath);
45457
45564
  const signingSession = await prepareBundleSigning(config.signing);
45458
- if (isSignedFileHash(bundle.fileHash)) {
45565
+ if (isSignedFileHash(bundle.manifestFileHash)) {
45459
45566
  if (!signingSession) throw new Error("Cannot copy a signed bundle without enabled bundle signing configuration.");
45460
45567
  if (!verifySignedFileHash({
45461
- actualFileHash: actualSourceFileHash,
45568
+ actualFileHash: actualManifestHash,
45462
45569
  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);
45570
+ signedFileHash: bundle.manifestFileHash
45571
+ })) throw new Error("Source manifest signature verification failed.");
45572
+ } else if (actualManifestHash !== bundle.manifestFileHash.toLowerCase()) throw new Error("Source manifest file hash verification failed.");
45573
+ const manifest = JSON.parse(await fs$2.readFile(sourceManifestPath, "utf8"));
45574
+ if (!manifest.assets || typeof manifest.assets !== "object") throw new Error(LEGACY_BUNDLE_ERROR);
45575
+ await downloadManifestAssets({
45576
+ bundle,
45577
+ manifest,
45578
+ outputDir: extractDir,
45579
+ storageAdapter,
45580
+ workDir
45581
+ });
45582
+ manifest.bundleId = nextBundleId;
45468
45583
  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));
45584
+ const sourceIsSigned = isSignedFileHash(getManifestFileHash(bundle));
45470
45585
  const manifestHasSignatures = assetPaths.some((assetPath) => Boolean(manifest.assets?.[assetPath]?.signature));
45471
45586
  if (!signingSession && (sourceIsSigned || manifestHasSignatures)) throw new Error("Cannot copy a signed bundle without enabled bundle signing configuration.");
45472
45587
  if (signingSession) {
@@ -45486,16 +45601,22 @@ async function createCopiedBundleArchive({ bundle, config, nextBundleId, storage
45486
45601
  manifest,
45487
45602
  workDir
45488
45603
  });
45604
+ const archivePath = path$1.join(workDir, "bundle.tar.br");
45605
+ manifest.archive = await createTarBrTargetFiles({
45606
+ outfile: archivePath,
45607
+ targetFiles: assetPaths.map((name) => ({
45608
+ name,
45609
+ path: resolveExtractedPath(extractDir, name)
45610
+ }))
45611
+ });
45489
45612
  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
45613
  const manifestHash = await getFileHash(manifestPath);
45494
- const nextFileHash = signingSession ? `${SIGNED_HASH_PREFIX}${await signingSession.signFileHash(fileHash)}` : fileHash;
45495
45614
  const nextManifestFileHash = signingSession ? `${SIGNED_HASH_PREFIX}${await signingSession.signFileHash(manifestHash)}` : manifestHash;
45496
- const archiveUpload = await putStorageFile(storagePlugin, createBundleStorageKey(nextBundleId), outputArchivePath);
45615
+ const archiveUpload = await putStorageFile(storageAdapter, createBundleStorageKey(nextBundleId), archivePath);
45497
45616
  uploadedStorageUris.push(archiveUpload.storageUri);
45498
- const assetBaseStorageUri = createStorageRootUriWithPath(archiveUpload.storageUri, nextBundleId, "assets");
45617
+ const manifestUpload = await putStorageFile(storageAdapter, createBundleStorageKey(nextBundleId), manifestPath);
45618
+ uploadedStorageUris.push(manifestUpload.storageUri);
45619
+ const assetBaseStorageUri = createStorageRootUriWithPath(manifestUpload.storageUri, nextBundleId, "assets");
45499
45620
  for (const assetUploadTarget of assetUploadTargets) {
45500
45621
  const storageUri = resolveManifestAssetStorageUri({
45501
45622
  assetBaseStorageUri,
@@ -45503,18 +45624,13 @@ async function createCopiedBundleArchive({ bundle, config, nextBundleId, storage
45503
45624
  downloadFileHash: assetUploadTarget.downloadFileHash,
45504
45625
  fileHash: assetUploadTarget.fileHash
45505
45626
  });
45506
- const { exists } = await storagePlugin.exists({ storageUri });
45507
- if (!exists) await putStorageFile(storagePlugin, getRelativeStorageDir(assetUploadTarget.storagePath) ? `assets/${getRelativeStorageDir(assetUploadTarget.storagePath)}` : "assets", assetUploadTarget.uploadSourcePath);
45627
+ const { exists } = await storageAdapter.exists({ storageUri });
45628
+ if (!exists) await putStorageFile(storageAdapter, getRelativeStorageDir(assetUploadTarget.storagePath) ? `assets/${getRelativeStorageDir(assetUploadTarget.storagePath)}` : "assets", assetUploadTarget.uploadSourcePath);
45508
45629
  }
45509
- const manifestUpload = await putStorageFile(storagePlugin, createBundleStorageKey(nextBundleId), manifestPath);
45510
- uploadedStorageUris.push(manifestUpload.storageUri);
45511
45630
  return {
45512
45631
  bundle: {
45513
45632
  ...bundle,
45514
45633
  id: nextBundleId,
45515
- archiveByteSize: archiveUpload.byteSize,
45516
- storageUri: archiveUpload.storageUri,
45517
- fileHash: nextFileHash,
45518
45634
  metadata: stripBundleArtifactMetadata(bundle.metadata),
45519
45635
  assetBaseStorageUri,
45520
45636
  patches: [],
@@ -45524,7 +45640,7 @@ async function createCopiedBundleArchive({ bundle, config, nextBundleId, storage
45524
45640
  uploadedStorageUris
45525
45641
  };
45526
45642
  } catch (error) {
45527
- await deleteUploadedCopy(storagePlugin, uploadedStorageUris);
45643
+ await deleteUploadedCopy(storageAdapter, uploadedStorageUris);
45528
45644
  throw error;
45529
45645
  } finally {
45530
45646
  await fs$2.rm(workDir, {
@@ -45533,12 +45649,12 @@ async function createCopiedBundleArchive({ bundle, config, nextBundleId, storage
45533
45649
  });
45534
45650
  }
45535
45651
  }
45536
- async function deleteUploadedCopy(storagePlugin, storageUris) {
45652
+ async function deleteUploadedCopy(storageAdapter, storageUris) {
45537
45653
  if (storageUris.length === 0) return;
45538
45654
  for (const storageUri of new Set(storageUris)) try {
45539
45655
  const protocol = new URL(storageUri).protocol.replace(":", "");
45540
- if (storagePlugin.protocol === protocol) await storagePlugin.delete({ storageUri });
45541
- else if (protocol !== "http" && protocol !== "https") throw new Error(`No storage plugin for protocol: ${protocol}`);
45656
+ if (storageAdapter.protocol === protocol) await storageAdapter.delete({ storageUri });
45657
+ else if (protocol !== "http" && protocol !== "https") throw new Error(`No storage adapter for protocol: ${protocol}`);
45542
45658
  } catch (error) {
45543
45659
  console.error("Failed to delete uploaded bundle copy:", error);
45544
45660
  }
@@ -45611,4 +45727,4 @@ function transformTemplate(templateString, values) {
45611
45727
  return result;
45612
45728
  }
45613
45729
  //#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 };
45730
+ export { BuildLogger, ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, InitEnvFileError, InitError, LEGACY_BUNDLE_ERROR, LegacyInfrastructureError, MissingInitInputsError, ServerDefinitionError, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, banner, clientAuthOf, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCopiedBundleArtifacts, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createMigrator, createTarBrTargetFiles, createZip, decryptJson, defineInitProvider, encryptJson, ensureInstallPackages, generateClientCredential, generateSchema, generatesSchema, getAndroidSdkPath, getBundleSigningPublicKey, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, getStorageFileByteSize, isServerDefinition, link, loadConfig, loadPlatformConfigs, log, makeEnv, moduleSpecifiersOf, p, prepareBundleSigning, printAppSetup, printBanner, provisionClientCredential, putStorageFile, readBundleSigningPublicKeyFile, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderAppImports, renderAppSetup, renderImportStatements, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolvePackageVersion, serverDefinitionOf, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig, writeHotUpdaterFiles, writeStorageFile, writeStorageResponseFile };