@hot-updater/cli-tools 0.35.8 → 0.35.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -296,6 +296,20 @@ declare const createHotUpdaterConfigScaffoldFromBuilder: (builder: ConfigBuilder
296
296
  }?: CreateHotUpdaterConfigScaffoldFromBuilderOptions) => HotUpdaterConfigScaffold;
297
297
  declare const writeHotUpdaterConfig: (scaffold: HotUpdaterConfigScaffold, filePath?: string) => Promise<WriteHotUpdaterConfigResult>;
298
298
  //#endregion
299
+ //#region src/hotUpdaterEnv.d.ts
300
+ type HotUpdaterInitEnv = {
301
+ readonly env: Readonly<Record<string, string>>;
302
+ readonly inputEnv?: Readonly<Record<string, string>>;
303
+ readonly managedEnv: Readonly<Record<string, string>>;
304
+ };
305
+ declare const getHotUpdaterInitInputEnv: ({
306
+ env,
307
+ managedEnv
308
+ }: HotUpdaterInitEnv, nonInteractive: boolean) => Readonly<Record<string, string>>;
309
+ declare const readHotUpdaterInitEnv: (cwd: string, envFile?: string) => Promise<HotUpdaterInitEnv>;
310
+ declare const readHotUpdaterEnv: (cwd: string) => Promise<Readonly<Record<string, string>>>;
311
+ declare const getHotUpdaterEnvValue: (env: Readonly<Record<string, string>>, key: string) => string | undefined;
312
+ //#endregion
299
313
  //#region src/HotUpdateDirUtil.d.ts
300
314
  declare const HotUpdateDirUtil: {
301
315
  readonly dirName: ".hot-updater";
@@ -320,6 +334,113 @@ declare const HotUpdateDirUtil: {
320
334
  }) => string;
321
335
  };
322
336
  //#endregion
337
+ //#region src/initProvider.d.ts
338
+ type InitProviderInputPersistence = "always" | "with-consent";
339
+ type InitProviderInputDefinition = {
340
+ readonly envKey: string;
341
+ readonly help: string;
342
+ readonly optional?: boolean;
343
+ readonly persistence?: InitProviderInputPersistence;
344
+ readonly preflight?: boolean;
345
+ readonly prompt?: {
346
+ readonly defaultValue?: string;
347
+ readonly message: string;
348
+ readonly placeholder?: string;
349
+ readonly type: "confirm" | "password" | "select" | "text";
350
+ };
351
+ readonly requiredWhen?: (inputs: Readonly<Record<string, string | undefined>>) => boolean;
352
+ readonly requirementHint?: string;
353
+ readonly validate?: (value: string | undefined) => boolean;
354
+ };
355
+ type InitProviderTextPromptDefinition = {
356
+ readonly defaultValue?: string;
357
+ readonly message: string;
358
+ readonly placeholder?: string;
359
+ readonly type: "text";
360
+ };
361
+ type InitProviderInputsDefinition = Readonly<Record<string, InitProviderInputDefinition>>;
362
+ type InitProviderDefinition<TInputs extends InitProviderInputsDefinition = InitProviderInputsDefinition> = {
363
+ readonly inputs: TInputs;
364
+ readonly label: string;
365
+ };
366
+ declare const defineInitProvider: <const TInputs extends InitProviderInputsDefinition>(provider: InitProviderDefinition<TInputs>) => InitProviderDefinition<TInputs>;
367
+ declare const shouldAutoSelectOnlyInitResource: ({
368
+ availableResourceCount,
369
+ savedIdentifier
370
+ }: {
371
+ readonly availableResourceCount: number;
372
+ readonly savedIdentifier?: string;
373
+ }) => boolean;
374
+ declare const getInitProviderTextPromptValues: (prompt: InitProviderTextPromptDefinition, savedValue?: string) => {
375
+ initialValue: string | undefined;
376
+ placeholder: string | undefined;
377
+ };
378
+ declare const resolveInitProviderInput: (env: Readonly<Record<string, string>>, input: InitProviderInputDefinition) => string | undefined;
379
+ declare const resolveInitProviderInputs: (env: Readonly<Record<string, string>>, provider: InitProviderDefinition) => Readonly<Record<string, string | undefined>>;
380
+ declare const getMissingInitProviderInputs: ({
381
+ inputs,
382
+ preflightOnly,
383
+ provider
384
+ }: {
385
+ readonly inputs: Readonly<Record<string, string | undefined>>;
386
+ readonly preflightOnly?: boolean;
387
+ readonly provider: InitProviderDefinition;
388
+ }) => readonly string[];
389
+ declare const assertInitProviderInputs: <TInputs extends InitProviderInputsDefinition>({
390
+ inputs,
391
+ provider,
392
+ strict
393
+ }: {
394
+ readonly inputs: Readonly<Record<string, string | undefined>>;
395
+ readonly provider: InitProviderDefinition<TInputs>;
396
+ readonly strict?: boolean;
397
+ }) => void;
398
+ declare const confirmInitInputPersistence: <TInputs extends InitProviderInputsDefinition>({
399
+ existingEnv,
400
+ inputs,
401
+ nonInteractive,
402
+ provider
403
+ }: {
404
+ readonly existingEnv: Readonly<Record<string, string>>;
405
+ readonly inputs: Readonly<Record<string, string | undefined>>;
406
+ readonly nonInteractive: boolean;
407
+ readonly provider: InitProviderDefinition<TInputs>;
408
+ }) => Promise<boolean>;
409
+ declare const getInitProviderEnvVars: <TInputs extends InitProviderInputsDefinition>({
410
+ includeConsentInputs,
411
+ inputs,
412
+ provider
413
+ }: {
414
+ readonly includeConsentInputs: boolean;
415
+ readonly inputs: Readonly<Record<string, string | undefined>>;
416
+ readonly provider: InitProviderDefinition<TInputs>;
417
+ }) => Record<string, string>;
418
+ //#endregion
419
+ //#region src/initOptions.d.ts
420
+ type RunInitOptions = {
421
+ readonly build: BuildType;
422
+ readonly envFile?: string;
423
+ };
424
+ declare class InitError extends Error {
425
+ readonly name: string;
426
+ }
427
+ declare class MissingInitInputsError extends InitError {
428
+ readonly missingInputs: readonly string[];
429
+ readonly name = "MissingInitInputsError";
430
+ constructor(missingInputs: readonly string[]);
431
+ }
432
+ declare class InitEnvFileError extends InitError {
433
+ readonly name = "InitEnvFileError";
434
+ }
435
+ declare const assertInitInputs: ({
436
+ inputs,
437
+ strict
438
+ }: {
439
+ readonly inputs: Readonly<Record<string, string | undefined>>;
440
+ readonly strict?: boolean;
441
+ }) => void;
442
+ declare const getMissingInitInputs: (inputs: Readonly<Record<string, string | undefined>>) => readonly string[];
443
+ //#endregion
323
444
  //#region src/LogWriter.d.ts
324
445
  type HotUpdaterLogWriter = {
325
446
  logFilePath: string | null;
@@ -360,7 +481,8 @@ type EnvVarValue = string | {
360
481
  value: string;
361
482
  };
362
483
  declare const makeEnv: (newEnvVars: Record<string, EnvVarValue>, filePath?: string, options?: {
363
- preserveKeys?: string[];
484
+ readonly preserveKeys?: readonly string[];
485
+ readonly removeKeys?: readonly string[];
364
486
  }) => Promise<string>;
365
487
  //#endregion
366
488
  //#region src/promoteBundle.d.ts
@@ -1687,4 +1809,4 @@ type TransformTemplateArgs<T extends string> = { [Key in ExtractPlaceholders<T>]
1687
1809
  */
1688
1810
  declare function transformTemplate<T extends string>(templateString: T, values: TransformTemplateArgs<T>): string;
1689
1811
  //#endregion
1690
- export { BuildLogger, BuildLoggerConfig, BuildType, ConfigBuilder, ConfigBuilderScaffold, ConfigResponse, CreateHotUpdaterConfigScaffoldFromBuilderOptions, CreateHotUpdaterConfigScaffoldOptions, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, HotUpdaterConfigOptions, HotUpdaterConfigScaffold, HotUpdaterLogWriter, IConfigBuilder, ImportInfo, LEGACY_BUNDLE_ERROR, ManagedHelperStatement, ManagedHelperStrategy, PromoteBundleDependencies, PromoteBundleInput, PromptProgress, PromptSpinner, ProviderConfig, ReactNativeMetadata, ReadPackageUpResult, WriteHotUpdaterConfigResult, banner, typedColors as colors, copyDirToTmp, createCopiedBundleArchive, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBr, createTarBrTargetFiles, createTarGz, createTarGzTargetFiles, createZip, createZipTargetFiles, decryptJson, encryptJson, ensureInstallPackages, getAndroidSdkPath, getCwd, getPackageManager, getReactNativeMetadatas, link, loadConfig, log, makeEnv, p, printBanner, promoteBundle, readPackageUp, renderImportStatements, resolveHotUpdaterServerVersion, resolvePackageVersion, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig };
1812
+ export { BuildLogger, BuildLoggerConfig, BuildType, ConfigBuilder, ConfigBuilderScaffold, ConfigResponse, CreateHotUpdaterConfigScaffoldFromBuilderOptions, CreateHotUpdaterConfigScaffoldOptions, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, HotUpdaterConfigOptions, HotUpdaterConfigScaffold, HotUpdaterInitEnv, HotUpdaterLogWriter, IConfigBuilder, ImportInfo, InitEnvFileError, InitError, InitProviderDefinition, InitProviderInputDefinition, InitProviderInputPersistence, LEGACY_BUNDLE_ERROR, ManagedHelperStatement, ManagedHelperStrategy, MissingInitInputsError, PromoteBundleDependencies, PromoteBundleInput, PromptProgress, PromptSpinner, ProviderConfig, ReactNativeMetadata, ReadPackageUpResult, RunInitOptions, WriteHotUpdaterConfigResult, assertInitInputs, assertInitProviderInputs, banner, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCopiedBundleArchive, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBr, createTarBrTargetFiles, createTarGz, createTarGzTargetFiles, createZip, createZipTargetFiles, decryptJson, defineInitProvider, encryptJson, ensureInstallPackages, getAndroidSdkPath, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, link, loadConfig, log, makeEnv, p, printBanner, promoteBundle, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderImportStatements, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolvePackageVersion, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig };
package/dist/index.mjs CHANGED
@@ -24,7 +24,7 @@ import { EventEmitter as EventEmitter$1, addAbortListener, on, once, setMaxListe
24
24
  import As, { Duplex, PassThrough as PassThrough$1, Readable as Readable$1, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
25
25
  import { StringDecoder } from "node:string_decoder";
26
26
  import no from "node:assert";
27
- import crypto$1, { randomBytes } from "node:crypto";
27
+ import crypto$1, { randomBytes, randomUUID } from "node:crypto";
28
28
  import fs$3 from "node:fs/promises";
29
29
  import { fileURLToPath } from "node:url";
30
30
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
@@ -32,7 +32,7 @@ import { scheduler, setImmediate as setImmediate$1, setTimeout as setTimeout$1 }
32
32
  import { serialize } from "node:v8";
33
33
  import { finished, pipeline as pipeline$1 } from "node:stream/promises";
34
34
  import { Buffer as Buffer$2 } from "node:buffer";
35
- import ts from "typescript";
35
+ import { parseSync } from "oxc-parser";
36
36
  import { loadConfig as loadConfig$1 } from "unconfig";
37
37
  import { brotliDecompressSync, createBrotliCompress as createBrotliCompress$1 } from "node:zlib";
38
38
  import { getManifestFileHash, stripBundleArtifactMetadata } from "@hot-updater/core";
@@ -22329,8 +22329,13 @@ const tasks = async (taskList, options) => {
22329
22329
  if (task.enabled === false) continue;
22330
22330
  const taskSpinner = spinner(options);
22331
22331
  taskSpinner.start(task.title);
22332
- const result = await task.task(taskSpinner.message);
22333
- taskSpinner.stop(result || task.title);
22332
+ try {
22333
+ const result = await task.task(taskSpinner.message);
22334
+ taskSpinner.stop(result || task.title);
22335
+ } catch (error) {
22336
+ taskSpinner.error(error instanceof Error ? error.message : String(error));
22337
+ throw error;
22338
+ }
22334
22339
  }
22335
22340
  };
22336
22341
  const p = {
@@ -26143,7 +26148,7 @@ var ti = [
26143
26148
  "?",
26144
26149
  ":"
26145
26150
  ], Qi = ti.map((s) => String.fromCodePoint(61440 + Number(s.codePointAt(0)))), vn = new Map(ti.map((s, t) => [s, Qi[t]])), Mn = new Map(Qi.map((s, t) => [s, ti[t]])), Ji = (s) => ti.reduce((t, e) => t.split(e).join(vn.get(e)), s), $s = (s) => Qi.reduce((t, e) => t.split(e).join(Mn.get(e)), s);
26146
- var er = (s, t) => t ? (s = f(s).replace(/^\.(\/|$)/, ""), mt(t) + "/" + s) : f(s), Bn = 16 * 1024 * 1024, Qs = Symbol("process"), Js = Symbol("file"), js = Symbol("directory"), ts$1 = Symbol("symlink"), tr = Symbol("hardlink"), ce = Symbol("header"), ei = Symbol("read"), es = Symbol("lstat"), ii = Symbol("onlstat"), is = Symbol("onread"), ss = Symbol("onreadlink"), rs = Symbol("openfile"), ns = Symbol("onopenfile"), pt = Symbol("close"), si = Symbol("mode"), os$1 = Symbol("awaitDrain"), ji = Symbol("ondrain"), X = Symbol("prefix"), fe = class extends A {
26151
+ var er = (s, t) => t ? (s = f(s).replace(/^\.(\/|$)/, ""), mt(t) + "/" + s) : f(s), Bn = 16 * 1024 * 1024, Qs = Symbol("process"), Js = Symbol("file"), js = Symbol("directory"), ts = Symbol("symlink"), tr = Symbol("hardlink"), ce = Symbol("header"), ei = Symbol("read"), es = Symbol("lstat"), ii = Symbol("onlstat"), is = Symbol("onread"), ss = Symbol("onreadlink"), rs = Symbol("openfile"), ns = Symbol("onopenfile"), pt = Symbol("close"), si = Symbol("mode"), os$1 = Symbol("awaitDrain"), ji = Symbol("ondrain"), X = Symbol("prefix"), fe = class extends A {
26147
26152
  path;
26148
26153
  portable;
26149
26154
  myuid = process.getuid && process.getuid() || 0;
@@ -26208,7 +26213,7 @@ var er = (s, t) => t ? (s = f(s).replace(/^\.(\/|$)/, ""), mt(t) + "/" + s) : f(
26208
26213
  switch (this.type) {
26209
26214
  case "File": return this[Js]();
26210
26215
  case "Directory": return this[js]();
26211
- case "SymbolicLink": return this[ts$1]();
26216
+ case "SymbolicLink": return this[ts]();
26212
26217
  default: return this.end();
26213
26218
  }
26214
26219
  }
@@ -26254,7 +26259,7 @@ var er = (s, t) => t ? (s = f(s).replace(/^\.(\/|$)/, ""), mt(t) + "/" + s) : f(
26254
26259
  if (!this.stat) throw new Error("cannot create directory entry without stat");
26255
26260
  this.path.slice(-1) !== "/" && (this.path += "/"), this.stat.size = 0, this[ce](), this.end();
26256
26261
  }
26257
- [ts$1]() {
26262
+ [ts]() {
26258
26263
  fs.readlink(this.absolute, (t, e) => {
26259
26264
  if (t) return this.emit("error", t);
26260
26265
  this[ss](e);
@@ -26343,7 +26348,7 @@ var er = (s, t) => t ? (s = f(s).replace(/^\.(\/|$)/, ""), mt(t) + "/" + s) : f(
26343
26348
  [es]() {
26344
26349
  this[ii](fs.lstatSync(this.absolute));
26345
26350
  }
26346
- [ts$1]() {
26351
+ [ts]() {
26347
26352
  this[ss](fs.readlinkSync(this.absolute));
26348
26353
  }
26349
26354
  [rs]() {
@@ -42796,11 +42801,12 @@ const readPackageUp = async (cwd) => {
42796
42801
  const ensureInstallPackages = async (packages, options) => {
42797
42802
  const { versionResolver = (pkg) => pkg } = options ?? {};
42798
42803
  const pkgJson = await readPackageUp(getCwd());
42804
+ const installedPackages = new Set([...Object.keys(pkgJson?.packageJson?.dependencies ?? {}), ...Object.keys(pkgJson?.packageJson?.devDependencies ?? {})]);
42799
42805
  const dependenciesToInstall = (packages.dependencies ?? []).filter((pkg) => {
42800
- return !pkgJson?.packageJson?.dependencies?.[pkg];
42806
+ return !installedPackages.has(pkg);
42801
42807
  });
42802
42808
  const devDependenciesToInstall = (packages.devDependencies ?? []).filter((pkg) => {
42803
- return !pkgJson?.packageJson?.devDependencies?.[pkg];
42809
+ return !installedPackages.has(pkg);
42804
42810
  });
42805
42811
  const packageManager = getPackageManager();
42806
42812
  await p.tasks([{
@@ -43235,8 +43241,7 @@ const getReactNativeMetadatas = (cwd) => {
43235
43241
  //#endregion
43236
43242
  //#region src/hotUpdaterConfig.ts
43237
43243
  const HOT_UPDATER_CONFIG_PATH = "hot-updater.config.ts";
43238
- const WRAP_PREFIX = "const __hotUpdaterValue = ";
43239
- const WRAP_SUFFIX = ";";
43244
+ const CONFIG_FILE_NAME = "hot-updater.config.ts";
43240
43245
  const MANAGED_IMPORT_PACKAGES = new Set([
43241
43246
  "dotenv",
43242
43247
  "firebase-admin",
@@ -43256,52 +43261,70 @@ const KNOWN_BUILD_CALLEES = new Set([
43256
43261
  "expo",
43257
43262
  "rock"
43258
43263
  ]);
43259
- const createSnippetSourceFile = (code) => ts.createSourceFile("hot-updater-config-snippet.ts", code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
43260
- const extractCallIdentifier = (initializer) => {
43261
- const match = /^\s*([A-Za-z_$][\w$]*)\s*\(/.exec(initializer);
43262
- if (!match) throw new Error(`Failed to extract call identifier from "${initializer}"`);
43263
- return match[1];
43264
- };
43265
- const wrapExpression = (expression) => `${WRAP_PREFIX}${expression}${WRAP_SUFFIX}`;
43266
- const getWrappedObjectLiteral = (text) => {
43267
- const sourceFile = createSnippetSourceFile(wrapExpression(text));
43268
- const statement = sourceFile.statements[0];
43269
- if (!statement || !ts.isVariableStatement(statement)) return null;
43270
- const declaration = statement.declarationList.declarations[0];
43271
- if (!declaration || !declaration.initializer) return null;
43272
- if (!ts.isObjectLiteralExpression(declaration.initializer)) return null;
43264
+ const parseConfigSource = (text) => {
43265
+ const result = parseSync(CONFIG_FILE_NAME, text, {
43266
+ astType: "js",
43267
+ lang: "ts",
43268
+ preserveParens: false,
43269
+ sourceType: "module",
43270
+ showSemanticErrors: false
43271
+ });
43272
+ if (result.errors.length > 0) return null;
43273
43273
  return {
43274
- sourceFile,
43275
- objectLiteral: declaration.initializer,
43276
- offset: 26
43277
- };
43278
- };
43279
- const parseVariableStatement = (code) => {
43280
- const sourceFile = createSnippetSourceFile(code);
43281
- const statement = sourceFile.statements.find((node) => ts.isVariableStatement(node));
43282
- if (!statement || !ts.isVariableStatement(statement)) return null;
43283
- const declaration = statement.declarationList.declarations[0];
43284
- if (!declaration || !ts.isIdentifier(declaration.name) || !declaration.initializer) return null;
43274
+ program: result.program,
43275
+ text
43276
+ };
43277
+ };
43278
+ const getNodeText = (source, node) => source.text.slice(node.start, node.end);
43279
+ const getTopLevelFullStart = (source, statement) => {
43280
+ const statementIndex = source.program.body.findIndex((candidate) => candidate === statement);
43281
+ if (statementIndex <= 0) return statementIndex === 0 ? 0 : statement.start;
43282
+ return source.program.body[statementIndex - 1]?.end ?? statement.start;
43283
+ };
43284
+ const getStatementText = (source, statement) => source.text.slice(getTopLevelFullStart(source, statement), statement.end).trim();
43285
+ const parseVariableStatement = (text) => {
43286
+ const source = parseConfigSource(text);
43287
+ if (!source) return null;
43288
+ const statement = source.program.body.find((candidate) => candidate.type === "VariableDeclaration");
43289
+ if (statement?.type !== "VariableDeclaration") return null;
43290
+ const declaration = statement.declarations[0];
43291
+ if (declaration?.id.type !== "Identifier" || !declaration.init) return null;
43285
43292
  return {
43286
- sourceFile,
43293
+ source,
43287
43294
  statement,
43288
43295
  declaration
43289
43296
  };
43290
43297
  };
43291
- const getPropertyName = (property) => {
43292
- if (ts.isIdentifier(property) || ts.isStringLiteral(property) || ts.isNumericLiteral(property)) return property.text;
43293
- if (ts.isSpreadAssignment(property)) return null;
43294
- if (ts.isShorthandPropertyAssignment(property)) return property.name.text;
43295
- if (ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property)) {
43296
- const { name } = property;
43297
- if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
43298
- if (ts.isNumericLiteral(name)) return name.text;
43299
- }
43298
+ const getConfigObjectExpression = (argument) => {
43299
+ const expression = argument?.type === "TSSatisfiesExpression" ? argument.expression : argument;
43300
+ return expression?.type === "ObjectExpression" ? expression : null;
43301
+ };
43302
+ const findDefineConfigObject = (source) => {
43303
+ const exportDeclaration = source.program.body.find((statement) => {
43304
+ if (statement.type !== "ExportDefaultDeclaration") return false;
43305
+ const declaration = statement.declaration;
43306
+ if (declaration.type !== "CallExpression" || declaration.callee.type !== "Identifier") return false;
43307
+ return declaration.callee.name === "defineConfig" && getConfigObjectExpression(declaration.arguments[0]) !== null;
43308
+ });
43309
+ if (exportDeclaration?.type !== "ExportDefaultDeclaration") return null;
43310
+ const declaration = exportDeclaration.declaration;
43311
+ if (declaration.type !== "CallExpression") return null;
43312
+ const objectExpression = getConfigObjectExpression(declaration.arguments[0]);
43313
+ if (!objectExpression) return null;
43314
+ return {
43315
+ exportDeclaration,
43316
+ objectExpression
43317
+ };
43318
+ };
43319
+ const getObjectPropertyName = (property) => {
43320
+ if (property.type === "SpreadElement" || property.computed) return null;
43321
+ const { key } = property;
43322
+ if (key.type === "Identifier") return key.name;
43323
+ if (key.type === "Literal" && (typeof key.value === "string" || typeof key.value === "number")) return String(key.value);
43300
43324
  return null;
43301
43325
  };
43302
- const trimStatementText = (text) => text.trim();
43303
- const getStatementText = (sourceText, statement) => trimStatementText(sourceText.slice(statement.getFullStart(), statement.end));
43304
- const getObjectTrailingComma = (text) => {
43326
+ const isDataProperty = (property) => property.type === "Property" && property.kind === "init" && !property.method && !property.shorthand;
43327
+ const hasTrailingComma = (text) => {
43305
43328
  const closeBraceIndex = text.lastIndexOf("}");
43306
43329
  if (closeBraceIndex === -1) return false;
43307
43330
  let index = closeBraceIndex - 1;
@@ -43310,7 +43333,7 @@ const getObjectTrailingComma = (text) => {
43310
43333
  };
43311
43334
  const dedentBlock = (text) => {
43312
43335
  const lines = text.replace(/\s+$/, "").split("\n");
43313
- const indents = lines.filter((line) => line.trim() !== "").map((line) => line.match(/^\s*/)[0].length);
43336
+ const indents = lines.filter((line) => line.trim() !== "").map((line) => line.match(/^\s*/)?.[0].length ?? 0);
43314
43337
  const minIndent = indents.length > 0 ? Math.min(...indents) : 0;
43315
43338
  return lines.map((line) => line.slice(minIndent)).join("\n");
43316
43339
  };
@@ -43322,88 +43345,73 @@ const appendMissingProperties = (objectText, propertyTexts, hasExistingPropertie
43322
43345
  const formattedProperties = propertyTexts.map((propertyText) => indentBlock(propertyText, childIndent)).join(",\n");
43323
43346
  const closeBraceIndex = objectText.lastIndexOf("}");
43324
43347
  if (closeBraceIndex === -1) return objectText;
43325
- const prefix = hasExistingProperties ? getObjectTrailingComma(objectText) ? "\n" : ",\n" : "\n";
43348
+ const prefix = hasExistingProperties ? hasTrailingComma(objectText) ? "\n" : ",\n" : "\n";
43326
43349
  const suffix = `,\n${closingIndent}`;
43327
43350
  return `${objectText.slice(0, closeBraceIndex)}${prefix}${formattedProperties}${suffix}${objectText.slice(closeBraceIndex)}`;
43328
43351
  };
43329
- const mergeObjectLiteralText = (existingText, newText) => {
43330
- const existingWrapped = getWrappedObjectLiteral(existingText);
43331
- const newWrapped = getWrappedObjectLiteral(newText);
43332
- if (!existingWrapped || !newWrapped) return null;
43352
+ const mergeObjectLiteralText = (existingObject, newObject) => {
43353
+ const existingText = getNodeText(existingObject.source, existingObject.objectExpression);
43333
43354
  const existingPropertyNames = /* @__PURE__ */ new Set();
43334
43355
  const existingSpreadTexts = /* @__PURE__ */ new Set();
43335
43356
  const edits = [];
43336
- for (const property of existingWrapped.objectLiteral.properties) {
43337
- if (ts.isSpreadAssignment(property)) {
43338
- existingSpreadTexts.add(property.expression.getText(existingWrapped.sourceFile).trim());
43357
+ for (const property of existingObject.objectExpression.properties) {
43358
+ if (property.type === "SpreadElement") {
43359
+ existingSpreadTexts.add(getNodeText(existingObject.source, property.argument).trim());
43339
43360
  continue;
43340
43361
  }
43341
- const propertyName = getPropertyName(property);
43362
+ const propertyName = getObjectPropertyName(property);
43342
43363
  if (!propertyName) continue;
43343
43364
  existingPropertyNames.add(propertyName);
43344
- const nextProperty = newWrapped.objectLiteral.properties.find((candidate) => {
43345
- if (ts.isSpreadAssignment(candidate)) return false;
43346
- return getPropertyName(candidate) === propertyName;
43347
- });
43348
- if (!nextProperty || !ts.isPropertyAssignment(property) || !ts.isPropertyAssignment(nextProperty)) continue;
43349
- if (ts.isObjectLiteralExpression(property.initializer) && ts.isObjectLiteralExpression(nextProperty.initializer)) {
43350
- const mergedInitializer = mergeObjectLiteralText(property.initializer.getText(existingWrapped.sourceFile), nextProperty.initializer.getText(newWrapped.sourceFile));
43351
- if (!mergedInitializer) return null;
43365
+ const nextProperty = newObject.objectExpression.properties.find((candidate) => getObjectPropertyName(candidate) === propertyName);
43366
+ if (!nextProperty || !isDataProperty(property) || !isDataProperty(nextProperty)) continue;
43367
+ if (property.value.type === "ObjectExpression" && nextProperty.value.type === "ObjectExpression") {
43368
+ const mergedValue = mergeObjectLiteralText({
43369
+ objectExpression: property.value,
43370
+ source: existingObject.source
43371
+ }, {
43372
+ objectExpression: nextProperty.value,
43373
+ source: newObject.source
43374
+ });
43375
+ if (!mergedValue) return null;
43352
43376
  edits.push({
43353
- start: property.initializer.getStart(existingWrapped.sourceFile) - existingWrapped.offset,
43354
- end: property.initializer.end - existingWrapped.offset,
43355
- text: mergedInitializer
43377
+ start: property.value.start - existingObject.objectExpression.start,
43378
+ end: property.value.end - existingObject.objectExpression.start,
43379
+ text: mergedValue
43356
43380
  });
43357
43381
  }
43358
43382
  }
43359
43383
  let mergedText = existingText;
43360
- for (const edit of edits.sort((a, b) => b.start - a.start)) mergedText = mergedText.slice(0, edit.start) + edit.text + mergedText.slice(edit.end);
43361
- const missingPropertyTexts = newWrapped.objectLiteral.properties.filter((property) => {
43362
- if (ts.isSpreadAssignment(property)) return !existingSpreadTexts.has(property.expression.getText(newWrapped.sourceFile).trim());
43363
- const propertyName = getPropertyName(property);
43384
+ for (const edit of edits.sort((left, right) => right.start - left.start)) mergedText = mergedText.slice(0, edit.start) + edit.text + mergedText.slice(edit.end);
43385
+ const missingPropertyTexts = newObject.objectExpression.properties.filter((property) => {
43386
+ if (property.type === "SpreadElement") return !existingSpreadTexts.has(getNodeText(newObject.source, property.argument).trim());
43387
+ const propertyName = getObjectPropertyName(property);
43364
43388
  return propertyName ? !existingPropertyNames.has(propertyName) : false;
43365
- }).map((property) => property.getText(newWrapped.sourceFile));
43366
- return appendMissingProperties(mergedText, missingPropertyTexts, existingWrapped.objectLiteral.properties.length > 0);
43367
- };
43368
- const buildMergedCallInitializer = (existingCall, existingSourceFile, newCall, newSourceFile) => {
43369
- const existingCallee = existingCall.expression.getText(existingSourceFile);
43370
- const [existingArg] = existingCall.arguments;
43371
- const [newArg] = newCall.arguments;
43372
- if (existingCall.arguments.length === 1 && newCall.arguments.length === 1 && existingArg && newArg && ts.isObjectLiteralExpression(existingArg) && ts.isObjectLiteralExpression(newArg)) {
43373
- const mergedObjectLiteral = mergeObjectLiteralText(existingArg.getText(existingSourceFile), newArg.getText(newSourceFile));
43389
+ }).map((property) => getNodeText(newObject.source, property));
43390
+ return appendMissingProperties(mergedText, missingPropertyTexts, existingObject.objectExpression.properties.length > 0);
43391
+ };
43392
+ const buildMergedCallInitializer = (existing, next) => {
43393
+ const [existingArgument] = existing.callExpression.arguments;
43394
+ const [nextArgument] = next.callExpression.arguments;
43395
+ if (existing.callExpression.arguments.length === 1 && next.callExpression.arguments.length === 1 && existingArgument?.type === "ObjectExpression" && nextArgument?.type === "ObjectExpression") {
43396
+ const mergedObjectLiteral = mergeObjectLiteralText({
43397
+ objectExpression: existingArgument,
43398
+ source: existing.source
43399
+ }, {
43400
+ objectExpression: nextArgument,
43401
+ source: next.source
43402
+ });
43374
43403
  if (!mergedObjectLiteral) return null;
43375
- return `${existingCallee}(${mergedObjectLiteral})`;
43404
+ return `${getNodeText(existing.source, existing.callExpression.callee)}(${mergedObjectLiteral})`;
43376
43405
  }
43377
- return existingCall.getText(existingSourceFile);
43406
+ return getNodeText(existing.source, existing.callExpression);
43378
43407
  };
43379
- const findDefineConfigObject = (sourceFile) => {
43380
- const exportAssignment = sourceFile.statements.find((statement) => {
43381
- if (!ts.isExportAssignment(statement)) return false;
43382
- const expression = statement.expression;
43383
- if (!ts.isCallExpression(expression)) return false;
43384
- return ts.isIdentifier(expression.expression) && expression.expression.text === "defineConfig" && expression.arguments.length > 0 && ts.isObjectLiteralExpression(expression.arguments[0]);
43385
- });
43386
- if (!exportAssignment || !ts.isExportAssignment(exportAssignment)) return null;
43387
- const expression = exportAssignment.expression;
43388
- if (!ts.isCallExpression(expression)) return null;
43389
- const [argument] = expression.arguments;
43390
- if (!argument || !ts.isObjectLiteralExpression(argument)) return null;
43391
- return {
43392
- exportAssignment,
43393
- objectLiteral: argument
43394
- };
43408
+ const findManagedProperty = (objectExpression, propertyName) => {
43409
+ const property = objectExpression.properties.find((candidate) => candidate.type === "Property" && candidate.kind === "init" && !candidate.method && !candidate.shorthand && getObjectPropertyName(candidate) === propertyName);
43410
+ return property?.type === "Property" ? property : null;
43395
43411
  };
43396
- const findManagedProperty = (objectLiteral, propertyName) => objectLiteral.properties.find((property) => ts.isPropertyAssignment(property) && getPropertyName(property.name) === propertyName);
43397
43412
  const getCallCallee = (expression) => {
43398
- if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression)) return null;
43399
- return expression.expression.text;
43400
- };
43401
- const isConfigCallStatement = (statement) => ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && ts.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "config";
43402
- const isManagedHelperStatement = (statement) => {
43403
- if (!ts.isVariableStatement(statement)) return null;
43404
- const declaration = statement.declarationList.declarations[0];
43405
- if (!declaration || !ts.isIdentifier(declaration.name)) return null;
43406
- return MANAGED_HELPER_NAMES.has(declaration.name.text) ? declaration.name.text : null;
43413
+ if (expression.type !== "CallExpression" || expression.callee.type !== "Identifier") return null;
43414
+ return expression.callee.name;
43407
43415
  };
43408
43416
  const mergeHelperStatement = (existingStatementText, helper) => {
43409
43417
  if (helper.strategy === "preserve-existing") return existingStatementText;
@@ -43411,16 +43419,22 @@ const mergeHelperStatement = (existingStatementText, helper) => {
43411
43419
  const existingStatement = parseVariableStatement(existingStatementText);
43412
43420
  const nextStatement = parseVariableStatement(helper.code);
43413
43421
  if (!existingStatement || !nextStatement) return null;
43414
- const existingInitializer = existingStatement.declaration.initializer;
43415
- const newInitializer = nextStatement.declaration.initializer;
43416
- if (!existingInitializer || !newInitializer || !ts.isObjectLiteralExpression(existingInitializer) || !ts.isObjectLiteralExpression(newInitializer)) return null;
43417
- const mergedInitializer = mergeObjectLiteralText(existingInitializer.getText(existingStatement.sourceFile), newInitializer.getText(nextStatement.sourceFile));
43422
+ const existingInitializer = existingStatement.declaration.init;
43423
+ const nextInitializer = nextStatement.declaration.init;
43424
+ if (existingInitializer?.type !== "ObjectExpression" || nextInitializer?.type !== "ObjectExpression") return null;
43425
+ const mergedInitializer = mergeObjectLiteralText({
43426
+ objectExpression: existingInitializer,
43427
+ source: existingStatement.source
43428
+ }, {
43429
+ objectExpression: nextInitializer,
43430
+ source: nextStatement.source
43431
+ });
43418
43432
  if (!mergedInitializer) return null;
43419
- return `${ts.tokenToString(existingStatement.statement.declarationList.flags & ts.NodeFlags.Const ? ts.SyntaxKind.ConstKeyword : existingStatement.statement.declarationList.flags & ts.NodeFlags.Let ? ts.SyntaxKind.LetKeyword : ts.SyntaxKind.VarKeyword) ?? "const"} ${helper.name} = ${mergedInitializer};`;
43433
+ return `${existingStatement.statement.kind === "let" || existingStatement.statement.kind === "var" ? existingStatement.statement.kind : "const"} ${helper.name} = ${mergedInitializer};`;
43420
43434
  };
43421
- const updateManagedObject = (existingText, existingObject, newObject, existingSourceFile, newSourceFile) => {
43422
- const objectStart = existingObject.getStart(existingSourceFile);
43423
- const objectText = existingText.slice(objectStart, existingObject.end);
43435
+ const updateManagedObject = (existing, next) => {
43436
+ const objectStart = existing.objectExpression.start;
43437
+ const objectText = getNodeText(existing.source, existing.objectExpression);
43424
43438
  const propertyEdits = [];
43425
43439
  const missingPropertyTexts = [];
43426
43440
  for (const propertyName of [
@@ -43428,85 +43442,133 @@ const updateManagedObject = (existingText, existingObject, newObject, existingSo
43428
43442
  "storage",
43429
43443
  "database"
43430
43444
  ]) {
43431
- const existingProperty = findManagedProperty(existingObject, propertyName);
43432
- const nextProperty = findManagedProperty(newObject, propertyName);
43445
+ const existingProperty = findManagedProperty(existing.objectExpression, propertyName);
43446
+ const nextProperty = findManagedProperty(next.objectExpression, propertyName);
43433
43447
  if (!nextProperty) continue;
43434
43448
  if (!existingProperty) {
43435
- missingPropertyTexts.push(nextProperty.getText(newSourceFile));
43449
+ missingPropertyTexts.push(getNodeText(next.source, nextProperty));
43436
43450
  continue;
43437
43451
  }
43438
- if (!ts.isCallExpression(existingProperty.initializer)) return null;
43439
- if (!ts.isCallExpression(nextProperty.initializer)) return null;
43440
- const existingCallee = getCallCallee(existingProperty.initializer);
43441
- const nextCallee = getCallCallee(nextProperty.initializer);
43452
+ const existingCallee = getCallCallee(existingProperty.value);
43453
+ const nextCallee = getCallCallee(nextProperty.value);
43442
43454
  if (!existingCallee || !nextCallee) return null;
43443
- let nextInitializerText = nextProperty.initializer.getText(newSourceFile);
43455
+ let nextInitializerText = getNodeText(next.source, nextProperty.value);
43444
43456
  if (propertyName === "build") {
43445
43457
  if (existingCallee === nextCallee) continue;
43446
43458
  if (!KNOWN_BUILD_CALLEES.has(existingCallee)) return null;
43447
43459
  } else if (existingCallee === nextCallee) {
43448
- const mergedInitializer = buildMergedCallInitializer(existingProperty.initializer, existingSourceFile, nextProperty.initializer, newSourceFile);
43449
- if (mergedInitializer === null) return null;
43460
+ if (existingProperty.value.type !== "CallExpression" || nextProperty.value.type !== "CallExpression") return null;
43461
+ const mergedInitializer = buildMergedCallInitializer({
43462
+ callExpression: existingProperty.value,
43463
+ source: existing.source
43464
+ }, {
43465
+ callExpression: nextProperty.value,
43466
+ source: next.source
43467
+ });
43468
+ if (!mergedInitializer) return null;
43450
43469
  nextInitializerText = mergedInitializer;
43451
43470
  }
43452
43471
  propertyEdits.push({
43453
- start: existingProperty.initializer.getStart(existingSourceFile) - objectStart,
43454
- end: existingProperty.initializer.end - objectStart,
43472
+ start: existingProperty.value.start - objectStart,
43473
+ end: existingProperty.value.end - objectStart,
43455
43474
  text: nextInitializerText
43456
43475
  });
43457
43476
  }
43458
43477
  let mergedText = objectText;
43459
- for (const edit of propertyEdits.sort((a, b) => b.start - a.start)) mergedText = mergedText.slice(0, edit.start) + edit.text + mergedText.slice(edit.end);
43460
- return appendMissingProperties(mergedText, missingPropertyTexts, existingObject.properties.length > 0);
43461
- };
43462
- const rebuildImportBlock = (sourceText, sourceFile, scaffold) => {
43463
- const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration);
43464
- if (importDeclarations.length === 0) return {
43478
+ for (const edit of propertyEdits.sort((left, right) => right.start - left.start)) mergedText = mergedText.slice(0, edit.start) + edit.text + mergedText.slice(edit.end);
43479
+ return appendMissingProperties(mergedText, missingPropertyTexts, existing.objectExpression.properties.length > 0);
43480
+ };
43481
+ const isConfigCallStatement = (statement) => statement.type === "ExpressionStatement" && statement.expression.type === "CallExpression" && statement.expression.callee.type === "Identifier" && statement.expression.callee.name === "config";
43482
+ const getManagedHelperName = (statement) => {
43483
+ if (statement.type !== "VariableDeclaration") return null;
43484
+ const declaration = statement.declarations[0];
43485
+ if (declaration?.id.type !== "Identifier") return null;
43486
+ return MANAGED_HELPER_NAMES.has(declaration.id.name) ? declaration.id.name : null;
43487
+ };
43488
+ const rebuildImportBlock = (source, scaffold) => {
43489
+ const importDeclarations = source.program.body.filter((statement) => statement.type === "ImportDeclaration");
43490
+ const firstImport = importDeclarations[0];
43491
+ const lastImport = importDeclarations.at(-1);
43492
+ if (!firstImport || !lastImport) return {
43465
43493
  start: 0,
43466
43494
  end: 0,
43467
43495
  text: `${renderImportStatements(scaffold.imports)}\n\n`
43468
43496
  };
43469
- const preservedImportTexts = importDeclarations.filter((declaration) => {
43470
- const moduleSpecifier = declaration.moduleSpecifier;
43471
- return ts.isStringLiteral(moduleSpecifier) && !MANAGED_IMPORT_PACKAGES.has(moduleSpecifier.text);
43472
- }).map((declaration) => trimStatementText(sourceText.slice(declaration.getFullStart(), declaration.end)));
43497
+ const preservedImportTexts = importDeclarations.filter((declaration) => !MANAGED_IMPORT_PACKAGES.has(declaration.source.value)).map((declaration) => source.text.slice(getTopLevelFullStart(source, declaration), declaration.end).trim());
43473
43498
  const managedImportText = renderImportStatements(scaffold.imports);
43474
43499
  const nextImportBlock = [...preservedImportTexts, managedImportText].filter(Boolean).join("\n");
43475
43500
  return {
43476
- start: importDeclarations[0].getFullStart(),
43477
- end: importDeclarations.at(-1).end,
43501
+ start: getTopLevelFullStart(source, firstImport),
43502
+ end: lastImport.end,
43478
43503
  text: `${nextImportBlock}\n\n`
43479
43504
  };
43480
43505
  };
43481
- const rebuildManagedBody = (sourceText, sourceFile, exportAssignment, scaffold) => {
43482
- const statementsBeforeExport = sourceFile.statements.filter((statement) => !ts.isImportDeclaration(statement) && statement.pos < exportAssignment.pos);
43506
+ const rebuildManagedBody = (source, exportStart, scaffold) => {
43507
+ const statementsBeforeExport = source.program.body.filter((statement) => statement.type !== "ImportDeclaration" && statement.start < exportStart);
43483
43508
  const managedHelpers = new Map(scaffold.helperStatements.map((statement) => [statement.name, statement]));
43484
43509
  const emittedHelpers = /* @__PURE__ */ new Set();
43485
43510
  const bodyStatements = [];
43486
43511
  for (const statement of statementsBeforeExport) {
43487
43512
  if (isConfigCallStatement(statement)) continue;
43488
- const helperName = isManagedHelperStatement(statement);
43513
+ const helperName = getManagedHelperName(statement);
43489
43514
  if (!helperName) {
43490
- bodyStatements.push(getStatementText(sourceText, statement));
43515
+ bodyStatements.push(getStatementText(source, statement));
43491
43516
  continue;
43492
43517
  }
43493
43518
  const helper = managedHelpers.get(helperName);
43494
43519
  if (!helper) continue;
43495
- const mergedHelperStatement = mergeHelperStatement(getStatementText(sourceText, statement), helper);
43496
- if (!mergedHelperStatement) return null;
43520
+ const mergedHelper = mergeHelperStatement(getStatementText(source, statement), helper);
43521
+ if (!mergedHelper) return null;
43497
43522
  emittedHelpers.add(helperName);
43498
- bodyStatements.push(mergedHelperStatement);
43523
+ bodyStatements.push(mergedHelper);
43499
43524
  }
43500
43525
  for (const helper of scaffold.helperStatements) if (!emittedHelpers.has(helper.name)) bodyStatements.push(helper.code.trim());
43501
43526
  const bodyText = bodyStatements.filter(Boolean).join("\n\n");
43502
43527
  const configStatement = `config({ path: ".env.hotupdater" });`;
43503
43528
  const managedBody = bodyText ? `\n\n${configStatement}\n\n${bodyText}\n\n` : `\n\n${configStatement}\n\n`;
43504
43529
  return {
43505
- start: sourceFile.statements.filter(ts.isImportDeclaration).at(-1)?.end ?? 0,
43506
- end: exportAssignment.getFullStart(),
43530
+ start: source.program.body.filter((statement) => statement.type === "ImportDeclaration").at(-1)?.end ?? 0,
43531
+ end: exportStart,
43507
43532
  text: managedBody
43508
43533
  };
43509
43534
  };
43535
+ const applyTextEdits = (sourceText, edits) => {
43536
+ let mergedText = sourceText;
43537
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) mergedText = mergedText.slice(0, edit.start) + edit.text + mergedText.slice(edit.end);
43538
+ return mergedText;
43539
+ };
43540
+ const mergeHotUpdaterConfigText = (existingText, scaffold) => {
43541
+ const existingSource = parseConfigSource(existingText);
43542
+ const nextSource = parseConfigSource(scaffold.text);
43543
+ if (!existingSource || !nextSource) return { reason: "Existing config is not a supported `export default defineConfig({ ... })` shape." };
43544
+ const existingConfig = findDefineConfigObject(existingSource);
43545
+ const nextConfig = findDefineConfigObject(nextSource);
43546
+ if (!existingConfig || !nextConfig) return { reason: "Existing config is not a supported `export default defineConfig({ ... })` shape." };
43547
+ const nextObjectText = updateManagedObject({
43548
+ objectExpression: existingConfig.objectExpression,
43549
+ source: existingSource
43550
+ }, {
43551
+ objectExpression: nextConfig.objectExpression,
43552
+ source: nextSource
43553
+ });
43554
+ if (!nextObjectText) return { reason: "Existing config uses dynamic build/storage/database expressions that cannot be merged safely." };
43555
+ const bodyEdit = rebuildManagedBody(existingSource, getTopLevelFullStart(existingSource, existingConfig.exportDeclaration), scaffold);
43556
+ if (!bodyEdit) return { reason: "Existing helper declarations could not be merged safely." };
43557
+ return { text: applyTextEdits(existingText, [
43558
+ {
43559
+ start: existingConfig.objectExpression.start,
43560
+ end: existingConfig.objectExpression.end,
43561
+ text: nextObjectText
43562
+ },
43563
+ bodyEdit,
43564
+ rebuildImportBlock(existingSource, scaffold)
43565
+ ]) };
43566
+ };
43567
+ const extractCallIdentifier = (initializer) => {
43568
+ const match = /^\s*([A-Za-z_$][\w$]*)\s*\(/.exec(initializer);
43569
+ if (!match) throw new Error(`Failed to extract call identifier from "${initializer}"`);
43570
+ return match[1];
43571
+ };
43510
43572
  const createHotUpdaterConfigScaffold = ({ build, storage, database, extraImports = [], helperStatements = [], updateStrategy = "appVersion" }) => {
43511
43573
  const intermediateCode = helperStatements.map((statement) => statement.code.trim()).filter(Boolean).join("\n\n");
43512
43574
  const builder = new ConfigBuilder().setBuildType(build).setStorage(storage).setDatabase(database);
@@ -43540,7 +43602,7 @@ const createHotUpdaterConfigScaffoldFromBuilder = (builder, { helperStatements =
43540
43602
  };
43541
43603
  const writeHotUpdaterConfig = async (scaffold, filePath = HOT_UPDATER_CONFIG_PATH) => {
43542
43604
  const existingText = await fs$1.readFile(filePath, "utf-8").catch((error) => {
43543
- if (error.code === "ENOENT") return null;
43605
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
43544
43606
  throw error;
43545
43607
  });
43546
43608
  if (existingText === null) {
@@ -43550,45 +43612,155 @@ const writeHotUpdaterConfig = async (scaffold, filePath = HOT_UPDATER_CONFIG_PAT
43550
43612
  path: filePath
43551
43613
  };
43552
43614
  }
43553
- const existingSourceFile = createSnippetSourceFile(existingText);
43554
- const existingConfig = findDefineConfigObject(existingSourceFile);
43555
- const nextSourceFile = createSnippetSourceFile(scaffold.text);
43556
- const nextConfig = findDefineConfigObject(nextSourceFile);
43557
- if (!existingConfig || !nextConfig) return {
43615
+ const mergeResult = mergeHotUpdaterConfigText(existingText, scaffold);
43616
+ if ("reason" in mergeResult) return {
43558
43617
  status: "skipped",
43559
43618
  path: filePath,
43560
- reason: "Existing config is not a supported `export default defineConfig({ ... })` shape."
43619
+ reason: mergeResult.reason
43561
43620
  };
43562
- const nextObjectText = updateManagedObject(existingText, existingConfig.objectLiteral, nextConfig.objectLiteral, existingSourceFile, nextSourceFile);
43563
- if (!nextObjectText) return {
43564
- status: "skipped",
43565
- path: filePath,
43566
- reason: "Existing config uses dynamic build/storage/database expressions that cannot be merged safely."
43621
+ await fs$1.writeFile(filePath, mergeResult.text, "utf-8");
43622
+ return {
43623
+ status: "merged",
43624
+ path: filePath
43567
43625
  };
43568
- const objectEdit = {
43569
- start: existingConfig.objectLiteral.getStart(existingSourceFile),
43570
- end: existingConfig.objectLiteral.end,
43571
- text: nextObjectText
43626
+ };
43627
+ //#endregion
43628
+ //#region src/initOptions.ts
43629
+ var InitError = class extends Error {
43630
+ name = "InitError";
43631
+ };
43632
+ var MissingInitInputsError = class extends InitError {
43633
+ name = "MissingInitInputsError";
43634
+ constructor(missingInputs) {
43635
+ super(["Init is missing required inputs:", ...missingInputs.map((input) => `- ${input}`)].join("\n"));
43636
+ this.missingInputs = missingInputs;
43637
+ }
43638
+ };
43639
+ var InitEnvFileError = class extends InitError {
43640
+ name = "InitEnvFileError";
43641
+ };
43642
+ const assertInitInputs = ({ inputs, strict }) => {
43643
+ if (!strict) return;
43644
+ const missingInputs = getMissingInitInputs(inputs);
43645
+ if (missingInputs.length > 0) throw new MissingInitInputsError(missingInputs);
43646
+ };
43647
+ const getMissingInitInputs = (inputs) => Object.entries(inputs).filter(([, value]) => !value?.trim()).map(([key]) => key);
43648
+ //#endregion
43649
+ //#region src/hotUpdaterEnv.ts
43650
+ const HOT_UPDATER_ENV_PATH = ".env.hotupdater";
43651
+ const unquoteEnvValue = (value) => {
43652
+ const trimmed = value.trim();
43653
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) try {
43654
+ const parsed = JSON.parse(trimmed);
43655
+ return typeof parsed === "string" ? parsed : trimmed.slice(1, -1);
43656
+ } catch {
43657
+ return trimmed.slice(1, -1);
43658
+ }
43659
+ return trimmed.startsWith("'") && trimmed.endsWith("'") ? trimmed.slice(1, -1) : trimmed;
43660
+ };
43661
+ const parseEnv = (content) => {
43662
+ const env = {};
43663
+ for (const line of content.split("\n")) {
43664
+ const trimmed = line.trim();
43665
+ if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
43666
+ const separatorIndex = trimmed.indexOf("=");
43667
+ const key = trimmed.slice(0, separatorIndex).trim();
43668
+ if (key) env[key] = unquoteEnvValue(trimmed.slice(separatorIndex + 1));
43669
+ }
43670
+ return env;
43671
+ };
43672
+ const readEnvFile = async (filePath, allowMissing) => {
43673
+ let content;
43674
+ try {
43675
+ content = await fs$1.readFile(filePath, "utf-8");
43676
+ } catch (error) {
43677
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
43678
+ if (allowMissing) return {};
43679
+ throw new InitEnvFileError(`Init environment file not found: ${filePath}`);
43680
+ }
43681
+ throw error;
43682
+ }
43683
+ return parseEnv(content);
43684
+ };
43685
+ const getHotUpdaterInitInputEnv = ({ env, managedEnv }, nonInteractive) => nonInteractive ? env : managedEnv;
43686
+ const readHotUpdaterInitEnv = async (cwd, envFile) => {
43687
+ const savedEnvPath = path.resolve(cwd, HOT_UPDATER_ENV_PATH);
43688
+ const savedEnv = await readEnvFile(savedEnvPath, true);
43689
+ if (!envFile) return {
43690
+ env: {},
43691
+ managedEnv: savedEnv
43572
43692
  };
43573
- const importEdit = rebuildImportBlock(existingText, existingSourceFile, scaffold);
43574
- const bodyEdit = rebuildManagedBody(existingText, existingSourceFile, existingConfig.exportAssignment, scaffold);
43575
- if (!bodyEdit) return {
43576
- status: "skipped",
43577
- path: filePath,
43578
- reason: "Existing helper declarations could not be merged safely."
43693
+ const inputEnvPath = path.resolve(cwd, envFile);
43694
+ if (inputEnvPath === savedEnvPath) return {
43695
+ env: savedEnv,
43696
+ managedEnv: savedEnv
43579
43697
  };
43580
- let mergedText = existingText;
43581
- for (const edit of [
43582
- objectEdit,
43583
- bodyEdit,
43584
- importEdit
43585
- ].sort((a, b) => b.start - a.start)) mergedText = mergedText.slice(0, edit.start) + edit.text + mergedText.slice(edit.end);
43586
- await fs$1.writeFile(filePath, mergedText, "utf-8");
43698
+ const inputEnv = await readEnvFile(inputEnvPath, false);
43587
43699
  return {
43588
- status: "merged",
43589
- path: filePath
43700
+ env: {
43701
+ ...savedEnv,
43702
+ ...inputEnv
43703
+ },
43704
+ inputEnv,
43705
+ managedEnv: savedEnv
43590
43706
  };
43591
43707
  };
43708
+ const readHotUpdaterEnv = async (cwd) => {
43709
+ const { managedEnv } = await readHotUpdaterInitEnv(cwd);
43710
+ return managedEnv;
43711
+ };
43712
+ const getHotUpdaterEnvValue = (env, key) => {
43713
+ if (process.env[key] !== void 0) return process.env[key]?.trim() || void 0;
43714
+ return env[key]?.trim() || void 0;
43715
+ };
43716
+ //#endregion
43717
+ //#region src/initProvider.ts
43718
+ const defineInitProvider = (provider) => provider;
43719
+ const shouldAutoSelectOnlyInitResource = ({ availableResourceCount, savedIdentifier }) => savedIdentifier === void 0 && availableResourceCount === 1;
43720
+ const getInitProviderTextPromptValues = (prompt, savedValue) => ({
43721
+ initialValue: savedValue ?? prompt.defaultValue,
43722
+ placeholder: prompt.placeholder ?? prompt.defaultValue
43723
+ });
43724
+ const resolveInitProviderInput = (env, input) => getHotUpdaterEnvValue(env, input.envKey);
43725
+ const resolveInitProviderInputs = (env, provider) => Object.fromEntries(Object.entries(provider.inputs).map(([name, input]) => [name, resolveInitProviderInput(env, input)]));
43726
+ const getMissingInitProviderInputs = ({ inputs, preflightOnly = false, provider }) => Object.entries(provider.inputs).filter(([name, input]) => {
43727
+ if (preflightOnly && input.preflight === false) return false;
43728
+ const required = input.requiredWhen ? input.requiredWhen(inputs) : !input.optional;
43729
+ const value = inputs[name];
43730
+ return required && (!value?.trim() || input.validate?.(value) === false);
43731
+ }).map(([, input]) => input.envKey);
43732
+ const assertInitProviderInputs = ({ inputs, provider, strict }) => {
43733
+ if (!strict) return;
43734
+ const missingInputs = getMissingInitProviderInputs({
43735
+ inputs,
43736
+ provider
43737
+ });
43738
+ if (missingInputs.length > 0) throw new MissingInitInputsError(missingInputs);
43739
+ };
43740
+ const hasNewConsentInput = (provider, inputs, existingEnv) => Object.entries(provider.inputs).some(([name, input]) => {
43741
+ if (input.persistence !== "with-consent") return false;
43742
+ const value = inputs[name];
43743
+ return value !== void 0 && value !== existingEnv[input.envKey]?.trim();
43744
+ });
43745
+ const confirmInitInputPersistence = async ({ existingEnv, inputs, nonInteractive, provider }) => {
43746
+ if (!hasNewConsentInput(provider, inputs, existingEnv)) return true;
43747
+ if (nonInteractive) return false;
43748
+ const confirmed = await p.confirm({
43749
+ message: "Save these init inputs to .env.hotupdater for future infrastructure updates?",
43750
+ initialValue: true
43751
+ });
43752
+ if (p.isCancel(confirmed)) process.exit(1);
43753
+ if (!confirmed) p.log.info("Credential inputs were not saved; provide them again for future infrastructure updates.");
43754
+ return confirmed;
43755
+ };
43756
+ const getInitProviderEnvVars = ({ includeConsentInputs, inputs, provider }) => {
43757
+ const env = {};
43758
+ for (const [name, input] of Object.entries(provider.inputs)) {
43759
+ const value = inputs[name];
43760
+ if (value !== void 0 && (input.persistence !== "with-consent" || includeConsentInputs)) env[input.envKey] = value;
43761
+ }
43762
+ return env;
43763
+ };
43592
43764
  //#endregion
43593
43765
  //#region ../../node_modules/.pnpm/es-toolkit@1.47.0/node_modules/es-toolkit/dist/predicate/isPlainObject.mjs
43594
43766
  /**
@@ -43766,7 +43938,7 @@ const getDefaultConfig = () => {
43766
43938
  releaseChannel: "production",
43767
43939
  updateStrategy: "appVersion",
43768
43940
  compressStrategy: "zip",
43769
- fingerprint: { extraSources: [] },
43941
+ fingerprint: {},
43770
43942
  patch: {
43771
43943
  enabled: true,
43772
43944
  maxBaseBundles: 3
@@ -43829,10 +44001,54 @@ const log = {
43829
44001
  };
43830
44002
  //#endregion
43831
44003
  //#region src/makeEnv.ts
44004
+ const isFileSystemError = (error) => error instanceof Error && "code" in error;
44005
+ const assertSafeExistingTarget = async (filePath) => {
44006
+ try {
44007
+ const target = await fs$3.lstat(filePath);
44008
+ if (target.isSymbolicLink() || !target.isFile()) throw new Error(`Refusing to write init environment values to a non-regular file: ${filePath}`);
44009
+ } catch (error) {
44010
+ if (isFileSystemError(error) && error.code === "ENOENT") return;
44011
+ throw error;
44012
+ }
44013
+ };
44014
+ const serializeEnvValue = (value) => {
44015
+ if (value.includes("\0") || value.includes("\r") || value.includes("\n")) throw new Error("Environment values cannot contain NUL or newlines.");
44016
+ return /^[A-Za-z0-9_./:@%+,=-]*$/.test(value) ? value : JSON.stringify(value);
44017
+ };
44018
+ const formatEnvLine = (key, value) => {
44019
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid environment variable name: ${key}`);
44020
+ return `${key}=${serializeEnvValue(value)}`;
44021
+ };
44022
+ const formatComment = (comment) => {
44023
+ if (comment.includes("\0") || comment.includes("\r") || comment.includes("\n")) throw new Error("Environment comments cannot contain NUL or newlines.");
44024
+ return `# ${comment}`;
44025
+ };
44026
+ const writeEnvAtomically = async (filePath, content) => {
44027
+ const resolvedPath = path$1.resolve(filePath);
44028
+ const temporaryPath = path$1.join(path$1.dirname(resolvedPath), `.${path$1.basename(resolvedPath)}.${randomUUID()}.tmp`);
44029
+ let renamed = false;
44030
+ try {
44031
+ await fs$3.writeFile(temporaryPath, content, {
44032
+ encoding: "utf-8",
44033
+ flag: "wx",
44034
+ mode: 384
44035
+ });
44036
+ await fs$3.rename(temporaryPath, resolvedPath);
44037
+ renamed = true;
44038
+ } finally {
44039
+ if (!renamed) await fs$3.rm(temporaryPath, { force: true });
44040
+ }
44041
+ };
43832
44042
  const makeEnv = async (newEnvVars, filePath = ".env.hotupdater", options) => {
43833
44043
  try {
44044
+ const resolvedFilePath = path$1.resolve(filePath);
44045
+ await assertSafeExistingTarget(resolvedFilePath);
43834
44046
  const preserveKeys = new Set(options?.preserveKeys ?? []);
43835
- const existingContent = await fs$1.readFile(filePath, "utf-8").catch(() => "");
44047
+ const removeKeys = new Set(options?.removeKeys ?? []);
44048
+ const existingContent = await fs$3.readFile(resolvedFilePath, "utf-8").catch((error) => {
44049
+ if (isFileSystemError(error) && error.code === "ENOENT") return "";
44050
+ throw error;
44051
+ });
43836
44052
  const lines = existingContent ? existingContent.split("\n") : [];
43837
44053
  const processedKeys = /* @__PURE__ */ new Set();
43838
44054
  const updatedLines = [];
@@ -43848,6 +44064,7 @@ const makeEnv = async (newEnvVars, filePath = ".env.hotupdater", options) => {
43848
44064
  const nextLine = (lines[i + 1] ?? "").trim();
43849
44065
  if (nextLine && !nextLine.startsWith("#") && nextLine.includes("=")) {
43850
44066
  const [possibleKey = ""] = nextLine.split("=");
44067
+ if (removeKeys.has(possibleKey.trim())) continue;
43851
44068
  if (Object.hasOwn(newEnvVars, possibleKey.trim()) && !preserveKeys.has(possibleKey.trim())) continue;
43852
44069
  }
43853
44070
  }
@@ -43857,6 +44074,7 @@ const makeEnv = async (newEnvVars, filePath = ".env.hotupdater", options) => {
43857
44074
  if (trimmedLine.includes("=")) {
43858
44075
  const [keyPart] = line.split("=");
43859
44076
  const key = keyPart?.trim() ?? "";
44077
+ if (removeKeys.has(key)) continue;
43860
44078
  if (Object.hasOwn(newEnvVars, key)) {
43861
44079
  processedKeys.add(key);
43862
44080
  if (preserveKeys.has(key)) {
@@ -43865,18 +44083,18 @@ const makeEnv = async (newEnvVars, filePath = ".env.hotupdater", options) => {
43865
44083
  }
43866
44084
  const newValue = newEnvVars[key];
43867
44085
  if (typeof newValue === "object" && newValue !== null) {
43868
- updatedLines.push(`# ${newValue.comment}`);
43869
- updatedLines.push(`${key}=${newValue.value}`);
43870
- } else updatedLines.push(`${key}=${newValue}`);
44086
+ updatedLines.push(formatComment(newValue.comment));
44087
+ updatedLines.push(formatEnvLine(key, newValue.value));
44088
+ } else updatedLines.push(formatEnvLine(key, newValue));
43871
44089
  } else updatedLines.push(line);
43872
44090
  } else updatedLines.push(line);
43873
44091
  }
43874
44092
  for (const [key, val] of Object.entries(newEnvVars)) if (!processedKeys.has(key)) if (typeof val === "object" && val !== null) {
43875
- updatedLines.push(`# ${val.comment}`);
43876
- updatedLines.push(`${key}=${val.value}`);
43877
- } else updatedLines.push(`${key}=${val}`);
44093
+ updatedLines.push(formatComment(val.comment));
44094
+ updatedLines.push(formatEnvLine(key, val.value));
44095
+ } else updatedLines.push(formatEnvLine(key, val));
43878
44096
  const updatedContent = updatedLines.join("\n");
43879
- await fs$1.writeFile(filePath, updatedContent, "utf-8");
44097
+ await writeEnvAtomically(resolvedFilePath, updatedContent);
43880
44098
  return updatedContent;
43881
44099
  } catch (error) {
43882
44100
  console.error("Error while updating .env.hotupdater file:", error);
@@ -44228,4 +44446,4 @@ function transformTemplate(templateString, values) {
44228
44446
  return result;
44229
44447
  }
44230
44448
  //#endregion
44231
- export { BuildLogger, ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, LEGACY_BUNDLE_ERROR, banner, typedColors as colors, copyDirToTmp, createCopiedBundleArchive, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBr, createTarBrTargetFiles, createTarGz, createTarGzTargetFiles, createZip, createZipTargetFiles, decryptJson, encryptJson, ensureInstallPackages, getAndroidSdkPath, getCwd, getPackageManager, getReactNativeMetadatas, link, loadConfig, log, makeEnv, p, printBanner, promoteBundle, readPackageUp, renderImportStatements, resolveHotUpdaterServerVersion, resolvePackageVersion, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig };
44449
+ export { BuildLogger, ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, InitEnvFileError, InitError, LEGACY_BUNDLE_ERROR, MissingInitInputsError, assertInitInputs, assertInitProviderInputs, banner, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCopiedBundleArchive, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBr, createTarBrTargetFiles, createTarGz, createTarGzTargetFiles, createZip, createZipTargetFiles, decryptJson, defineInitProvider, encryptJson, ensureInstallPackages, getAndroidSdkPath, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, link, loadConfig, log, makeEnv, p, printBanner, promoteBundle, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderImportStatements, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolvePackageVersion, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/cli-tools",
3
- "version": "0.35.8",
3
+ "version": "0.35.10",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
@@ -46,11 +46,11 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "oxc-transform": "0.121.0",
50
- "typescript": "6.0.2",
49
+ "oxc-parser": "0.141.0",
50
+ "oxc-transform": "0.141.0",
51
51
  "unconfig": "7.5.0",
52
- "@hot-updater/core": "0.35.8",
53
- "@hot-updater/plugin-core": "0.35.8"
52
+ "@hot-updater/core": "0.35.10",
53
+ "@hot-updater/plugin-core": "0.35.10"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@clack/prompts": "1.7.0",
@@ -65,7 +65,7 @@
65
65
  "semver": "^7.6.3",
66
66
  "tar": "^7.5.16",
67
67
  "workspace-tools": "^0.41.7",
68
- "@hot-updater/test-utils": "0.35.8"
68
+ "@hot-updater/test-utils": "0.35.10"
69
69
  },
70
70
  "inlinedDependencies": {
71
71
  "@babel/code-frame": "7.29.0",