@hot-updater/cli-tools 0.29.5 → 0.29.6

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
@@ -24,13 +24,14 @@ import Stream, { Duplex, PassThrough as PassThrough$1, Readable as Readable$1, T
24
24
  import { StringDecoder } from "node:string_decoder";
25
25
  import assert$1 from "node:assert";
26
26
  import { randomBytes } from "node:crypto";
27
- import fsp from "node:fs/promises";
27
+ import fsPromises from "node:fs/promises";
28
28
  import { fileURLToPath } from "node:url";
29
29
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
30
30
  import { scheduler, setImmediate as setImmediate$1, setTimeout as setTimeout$1 } from "node:timers/promises";
31
31
  import { serialize } from "node:v8";
32
32
  import { finished } from "node:stream/promises";
33
33
  import { Buffer as Buffer$2 } from "node:buffer";
34
+ import ts from "typescript";
34
35
  import { loadConfig as loadConfig$1 } from "unconfig";
35
36
  import { transformSync } from "oxc-transform";
36
37
  //#region ../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
@@ -21882,6 +21883,53 @@ const printBanner = (version) => {
21882
21883
  };
21883
21884
  //#endregion
21884
21885
  //#region src/ConfigBuilder.ts
21886
+ const normalizeImportInfos = (imports) => {
21887
+ const collectedImports = /* @__PURE__ */ new Map();
21888
+ for (const info of imports) {
21889
+ const existing = collectedImports.get(info.pkg);
21890
+ if (existing) {
21891
+ if (info.named) for (const namedImport of info.named) existing.named.add(namedImport);
21892
+ if (info.defaultOrNamespace && !existing.defaultOrNamespace) existing.defaultOrNamespace = info.defaultOrNamespace;
21893
+ if (info.sideEffect && !existing.sideEffect) existing.sideEffect = true;
21894
+ continue;
21895
+ }
21896
+ collectedImports.set(info.pkg, {
21897
+ named: new Set(info.named ?? []),
21898
+ defaultOrNamespace: info.defaultOrNamespace,
21899
+ sideEffect: info.sideEffect ?? false
21900
+ });
21901
+ }
21902
+ return Array.from(collectedImports.entries()).sort(([a], [b]) => {
21903
+ const isABuild = a.startsWith("@hot-updater/");
21904
+ if (isABuild !== b.startsWith("@hot-updater/")) return isABuild ? -1 : 1;
21905
+ if (a === "dotenv/config") return -1;
21906
+ if (b === "dotenv/config") return 1;
21907
+ const isAdminA = a === "firebase-admin";
21908
+ if (isAdminA !== (b === "firebase-admin")) return isAdminA ? -1 : 1;
21909
+ return a.localeCompare(b);
21910
+ }).map(([pkg, info]) => ({
21911
+ pkg,
21912
+ named: Array.from(info.named).sort(),
21913
+ defaultOrNamespace: info.defaultOrNamespace,
21914
+ sideEffect: info.sideEffect ?? false
21915
+ }));
21916
+ };
21917
+ const renderImportStatements = (imports) => {
21918
+ const importLines = [];
21919
+ for (const info of normalizeImportInfos(imports)) {
21920
+ if (info.sideEffect) {
21921
+ importLines.push(`import "${info.pkg}";`);
21922
+ continue;
21923
+ }
21924
+ if (info.defaultOrNamespace) {
21925
+ if (info.pkg === "firebase-admin" && (info.named?.length ?? 0) > 0) importLines.push(`import ${info.defaultOrNamespace}, { ${info.named.join(", ")} } from "${info.pkg}";`);
21926
+ else importLines.push(`import ${info.defaultOrNamespace} from "${info.pkg}";`);
21927
+ continue;
21928
+ }
21929
+ if ((info.named?.length ?? 0) > 0) importLines.push(`import { ${info.named.join(", ")} } from "${info.pkg}";`);
21930
+ }
21931
+ return importLines.join("\n");
21932
+ };
21885
21933
  var ConfigBuilder = class {
21886
21934
  buildType = null;
21887
21935
  storageInfo = null;
@@ -21915,28 +21963,13 @@ var ConfigBuilder = class {
21915
21963
  addImports(imports) {
21916
21964
  for (const imp of imports) this.addImport(imp);
21917
21965
  }
21918
- generateImportStatements() {
21919
- const importLines = [];
21920
- const sortedPackages = Array.from(this.collectedImports.keys()).sort((a, b) => {
21921
- const isABuild = a.startsWith("@hot-updater/");
21922
- if (isABuild !== b.startsWith("@hot-updater/")) return isABuild ? -1 : 1;
21923
- if (a === "dotenv/config") return -1;
21924
- if (b === "dotenv/config") return 1;
21925
- const isAdminA = a === "firebase-admin";
21926
- if (isAdminA !== (b === "firebase-admin")) return isAdminA ? -1 : 1;
21927
- return a.localeCompare(b);
21928
- });
21929
- for (const pkg of sortedPackages) {
21930
- const info = this.collectedImports.get(pkg);
21931
- if (info.sideEffect) importLines.push(`import "${pkg}";`);
21932
- else if (info.defaultOrNamespace) if (pkg === "firebase-admin" && info.named.size > 0) importLines.push(`import ${info.defaultOrNamespace}, { ${Array.from(info.named).sort().join(", ")} } from "${pkg}";`);
21933
- else importLines.push(`import ${info.defaultOrNamespace} from "${pkg}";`);
21934
- else if (info.named.size > 0) {
21935
- const namedPart = Array.from(info.named).sort().join(", ");
21936
- importLines.push(`import { ${namedPart} } from "${pkg}";`);
21937
- }
21938
- }
21939
- return importLines.join("\n");
21966
+ getImportInfos() {
21967
+ return normalizeImportInfos(Array.from(this.collectedImports.entries()).map(([pkg, info]) => ({
21968
+ pkg,
21969
+ named: Array.from(info.named),
21970
+ defaultOrNamespace: info.defaultOrNamespace,
21971
+ sideEffect: info.sideEffect ?? false
21972
+ })));
21940
21973
  }
21941
21974
  generateBuildConfigString() {
21942
21975
  if (!this.buildType) throw new Error("Build type must be set using .setBuildType()");
@@ -21978,13 +22011,14 @@ var ConfigBuilder = class {
21978
22011
  this.intermediateCode = code.trim();
21979
22012
  return this;
21980
22013
  }
21981
- getResult() {
22014
+ getScaffold() {
21982
22015
  if (!this.buildType) throw new Error("Build type must be set using .setBuildType()");
21983
22016
  if (!this.storageInfo) throw new Error("Storage config must be set using .setStorage()");
21984
22017
  if (!this.databaseInfo) throw new Error("Database config must be set using .setDatabase()");
21985
- const importStatements = this.generateImportStatements();
22018
+ const imports = this.getImportInfos();
22019
+ const importStatements = renderImportStatements(imports);
21986
22020
  const buildConfigString = this.generateBuildConfigString();
21987
- return `
22021
+ const text = `
21988
22022
  ${importStatements}
21989
22023
 
21990
22024
  config({ path: ".env.hotupdater" });
@@ -21997,6 +22031,17 @@ export default defineConfig({
21997
22031
  updateStrategy: "appVersion", // or "fingerprint"
21998
22032
  });
21999
22033
  `.trim();
22034
+ return {
22035
+ imports,
22036
+ buildConfigString,
22037
+ storageConfigString: this.storageInfo.configString,
22038
+ databaseConfigString: this.databaseInfo.configString,
22039
+ intermediateCode: this.intermediateCode,
22040
+ text
22041
+ };
22042
+ }
22043
+ getResult() {
22044
+ return this.getScaffold().text;
22000
22045
  }
22001
22046
  };
22002
22047
  //#endregion
@@ -25943,7 +25988,7 @@ const mkdir = (dir, opt, cb) => {
25943
25988
  else cb();
25944
25989
  };
25945
25990
  if (dir === cwd) return checkCwd(dir, done);
25946
- if (preserve) return fsp.mkdir(dir, {
25991
+ if (preserve) return fsPromises.mkdir(dir, {
25947
25992
  mode,
25948
25993
  recursive: true
25949
25994
  }).then((made) => done(null, made ?? void 0), done);
@@ -42010,7 +42055,7 @@ async function findUp(name, { cwd = N.cwd(), type = "file", stopAt } = {}) {
42010
42055
  while (directory) {
42011
42056
  const filePath = isAbsoluteName ? name : path$1.join(directory, name);
42012
42057
  try {
42013
- const stats = await fsp.stat(filePath);
42058
+ const stats = await fsPromises.stat(filePath);
42014
42059
  if (type === "file" && stats.isFile() || type === "directory" && stats.isDirectory()) return filePath;
42015
42060
  } catch {}
42016
42061
  if (directory === stopAt || directory === root) break;
@@ -47079,7 +47124,7 @@ const _readPackage = (file, normalize) => {
47079
47124
  return json;
47080
47125
  };
47081
47126
  async function readPackage({ cwd, normalize = true } = {}) {
47082
- return _readPackage(await fsp.readFile(getPackagePath(cwd), "utf8"), normalize);
47127
+ return _readPackage(await fsPromises.readFile(getPackagePath(cwd), "utf8"), normalize);
47083
47128
  }
47084
47129
  //#endregion
47085
47130
  //#region ../../node_modules/.pnpm/read-package-up@11.0.0/node_modules/read-package-up/index.js
@@ -47538,6 +47583,363 @@ const getReactNativeMetadatas = (cwd) => {
47538
47583
  }
47539
47584
  };
47540
47585
  //#endregion
47586
+ //#region src/hotUpdaterConfig.ts
47587
+ const HOT_UPDATER_CONFIG_PATH = "hot-updater.config.ts";
47588
+ const WRAP_PREFIX = "const __hotUpdaterValue = ";
47589
+ const WRAP_SUFFIX = ";";
47590
+ const MANAGED_IMPORT_PACKAGES = new Set([
47591
+ "dotenv",
47592
+ "firebase-admin",
47593
+ "hot-updater",
47594
+ "@aws-sdk/credential-provider-sso",
47595
+ "@hot-updater/aws",
47596
+ "@hot-updater/bare",
47597
+ "@hot-updater/cloudflare",
47598
+ "@hot-updater/expo",
47599
+ "@hot-updater/firebase",
47600
+ "@hot-updater/rock",
47601
+ "@hot-updater/supabase"
47602
+ ]);
47603
+ const MANAGED_HELPER_NAMES = new Set(["commonOptions", "credential"]);
47604
+ const KNOWN_BUILD_CALLEES = new Set([
47605
+ "bare",
47606
+ "expo",
47607
+ "rock"
47608
+ ]);
47609
+ const createSnippetSourceFile = (code) => ts.createSourceFile("hot-updater-config-snippet.ts", code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
47610
+ const extractCallIdentifier = (initializer) => {
47611
+ const match = /^\s*([A-Za-z_$][\w$]*)\s*\(/.exec(initializer);
47612
+ if (!match) throw new Error(`Failed to extract call identifier from "${initializer}"`);
47613
+ return match[1];
47614
+ };
47615
+ const wrapExpression = (expression) => `${WRAP_PREFIX}${expression}${WRAP_SUFFIX}`;
47616
+ const getWrappedObjectLiteral = (text) => {
47617
+ const sourceFile = createSnippetSourceFile(wrapExpression(text));
47618
+ const statement = sourceFile.statements[0];
47619
+ if (!statement || !ts.isVariableStatement(statement)) return null;
47620
+ const declaration = statement.declarationList.declarations[0];
47621
+ if (!declaration || !declaration.initializer) return null;
47622
+ if (!ts.isObjectLiteralExpression(declaration.initializer)) return null;
47623
+ return {
47624
+ sourceFile,
47625
+ objectLiteral: declaration.initializer,
47626
+ offset: 26
47627
+ };
47628
+ };
47629
+ const parseVariableStatement = (code) => {
47630
+ const sourceFile = createSnippetSourceFile(code);
47631
+ const statement = sourceFile.statements.find((node) => ts.isVariableStatement(node));
47632
+ if (!statement || !ts.isVariableStatement(statement)) return null;
47633
+ const declaration = statement.declarationList.declarations[0];
47634
+ if (!declaration || !ts.isIdentifier(declaration.name) || !declaration.initializer) return null;
47635
+ return {
47636
+ sourceFile,
47637
+ statement,
47638
+ declaration
47639
+ };
47640
+ };
47641
+ const getPropertyName = (property) => {
47642
+ if (ts.isIdentifier(property) || ts.isStringLiteral(property) || ts.isNumericLiteral(property)) return property.text;
47643
+ if (ts.isSpreadAssignment(property)) return null;
47644
+ if (ts.isShorthandPropertyAssignment(property)) return property.name.text;
47645
+ if (ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property)) {
47646
+ const { name } = property;
47647
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
47648
+ if (ts.isNumericLiteral(name)) return name.text;
47649
+ }
47650
+ return null;
47651
+ };
47652
+ const trimStatementText = (text) => text.trim();
47653
+ const getStatementText = (sourceText, statement) => trimStatementText(sourceText.slice(statement.getFullStart(), statement.end));
47654
+ const getObjectTrailingComma = (text) => {
47655
+ const closeBraceIndex = text.lastIndexOf("}");
47656
+ if (closeBraceIndex === -1) return false;
47657
+ let index = closeBraceIndex - 1;
47658
+ while (index >= 0 && /\s/.test(text[index] ?? "")) index -= 1;
47659
+ return (text[index] ?? "") === ",";
47660
+ };
47661
+ const dedentBlock = (text) => {
47662
+ const lines = text.replace(/\s+$/, "").split("\n");
47663
+ const indents = lines.filter((line) => line.trim() !== "").map((line) => line.match(/^\s*/)[0].length);
47664
+ const minIndent = indents.length > 0 ? Math.min(...indents) : 0;
47665
+ return lines.map((line) => line.slice(minIndent)).join("\n");
47666
+ };
47667
+ const indentBlock = (text, indent) => dedentBlock(text).split("\n").map((line) => `${indent}${line}`).join("\n");
47668
+ const appendMissingProperties = (objectText, propertyTexts, hasExistingProperties) => {
47669
+ if (propertyTexts.length === 0) return objectText;
47670
+ const closingIndent = /\n([ \t]*)\}$/.exec(objectText)?.[1] ?? "";
47671
+ const childIndent = objectText.match(/\n([ \t]+)[^\s]/)?.[1] ?? `${closingIndent} `;
47672
+ const formattedProperties = propertyTexts.map((propertyText) => indentBlock(propertyText, childIndent)).join(",\n");
47673
+ const closeBraceIndex = objectText.lastIndexOf("}");
47674
+ if (closeBraceIndex === -1) return objectText;
47675
+ const prefix = hasExistingProperties ? getObjectTrailingComma(objectText) ? "\n" : ",\n" : "\n";
47676
+ const suffix = `,\n${closingIndent}`;
47677
+ return `${objectText.slice(0, closeBraceIndex)}${prefix}${formattedProperties}${suffix}${objectText.slice(closeBraceIndex)}`;
47678
+ };
47679
+ const mergeObjectLiteralText = (existingText, newText) => {
47680
+ const existingWrapped = getWrappedObjectLiteral(existingText);
47681
+ const newWrapped = getWrappedObjectLiteral(newText);
47682
+ if (!existingWrapped || !newWrapped) return null;
47683
+ const existingPropertyNames = /* @__PURE__ */ new Set();
47684
+ const existingSpreadTexts = /* @__PURE__ */ new Set();
47685
+ const edits = [];
47686
+ for (const property of existingWrapped.objectLiteral.properties) {
47687
+ if (ts.isSpreadAssignment(property)) {
47688
+ existingSpreadTexts.add(property.expression.getText(existingWrapped.sourceFile).trim());
47689
+ continue;
47690
+ }
47691
+ const propertyName = getPropertyName(property);
47692
+ if (!propertyName) continue;
47693
+ existingPropertyNames.add(propertyName);
47694
+ const nextProperty = newWrapped.objectLiteral.properties.find((candidate) => {
47695
+ if (ts.isSpreadAssignment(candidate)) return false;
47696
+ return getPropertyName(candidate) === propertyName;
47697
+ });
47698
+ if (!nextProperty || !ts.isPropertyAssignment(property) || !ts.isPropertyAssignment(nextProperty)) continue;
47699
+ if (ts.isObjectLiteralExpression(property.initializer) && ts.isObjectLiteralExpression(nextProperty.initializer)) {
47700
+ const mergedInitializer = mergeObjectLiteralText(property.initializer.getText(existingWrapped.sourceFile), nextProperty.initializer.getText(newWrapped.sourceFile));
47701
+ if (!mergedInitializer) return null;
47702
+ edits.push({
47703
+ start: property.initializer.getStart(existingWrapped.sourceFile) - existingWrapped.offset,
47704
+ end: property.initializer.end - existingWrapped.offset,
47705
+ text: mergedInitializer
47706
+ });
47707
+ }
47708
+ }
47709
+ let mergedText = existingText;
47710
+ for (const edit of edits.sort((a, b) => b.start - a.start)) mergedText = mergedText.slice(0, edit.start) + edit.text + mergedText.slice(edit.end);
47711
+ const missingPropertyTexts = newWrapped.objectLiteral.properties.filter((property) => {
47712
+ if (ts.isSpreadAssignment(property)) return !existingSpreadTexts.has(property.expression.getText(newWrapped.sourceFile).trim());
47713
+ const propertyName = getPropertyName(property);
47714
+ return propertyName ? !existingPropertyNames.has(propertyName) : false;
47715
+ }).map((property) => property.getText(newWrapped.sourceFile));
47716
+ return appendMissingProperties(mergedText, missingPropertyTexts, existingWrapped.objectLiteral.properties.length > 0);
47717
+ };
47718
+ const buildMergedCallInitializer = (existingCall, existingSourceFile, newCall, newSourceFile) => {
47719
+ const existingCallee = existingCall.expression.getText(existingSourceFile);
47720
+ const [existingArg] = existingCall.arguments;
47721
+ const [newArg] = newCall.arguments;
47722
+ if (existingCall.arguments.length === 1 && newCall.arguments.length === 1 && existingArg && newArg && ts.isObjectLiteralExpression(existingArg) && ts.isObjectLiteralExpression(newArg)) {
47723
+ const mergedObjectLiteral = mergeObjectLiteralText(existingArg.getText(existingSourceFile), newArg.getText(newSourceFile));
47724
+ if (!mergedObjectLiteral) return null;
47725
+ return `${existingCallee}(${mergedObjectLiteral})`;
47726
+ }
47727
+ return existingCall.getText(existingSourceFile);
47728
+ };
47729
+ const findDefineConfigObject = (sourceFile) => {
47730
+ const exportAssignment = sourceFile.statements.find((statement) => {
47731
+ if (!ts.isExportAssignment(statement)) return false;
47732
+ const expression = statement.expression;
47733
+ if (!ts.isCallExpression(expression)) return false;
47734
+ return ts.isIdentifier(expression.expression) && expression.expression.text === "defineConfig" && expression.arguments.length > 0 && ts.isObjectLiteralExpression(expression.arguments[0]);
47735
+ });
47736
+ if (!exportAssignment || !ts.isExportAssignment(exportAssignment)) return null;
47737
+ const expression = exportAssignment.expression;
47738
+ if (!ts.isCallExpression(expression)) return null;
47739
+ const [argument] = expression.arguments;
47740
+ if (!argument || !ts.isObjectLiteralExpression(argument)) return null;
47741
+ return {
47742
+ exportAssignment,
47743
+ objectLiteral: argument
47744
+ };
47745
+ };
47746
+ const findManagedProperty = (objectLiteral, propertyName) => objectLiteral.properties.find((property) => ts.isPropertyAssignment(property) && getPropertyName(property.name) === propertyName);
47747
+ const getCallCallee = (expression) => {
47748
+ if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression)) return null;
47749
+ return expression.expression.text;
47750
+ };
47751
+ const isConfigCallStatement = (statement) => ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && ts.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "config";
47752
+ const isManagedHelperStatement = (statement) => {
47753
+ if (!ts.isVariableStatement(statement)) return null;
47754
+ const declaration = statement.declarationList.declarations[0];
47755
+ if (!declaration || !ts.isIdentifier(declaration.name)) return null;
47756
+ return MANAGED_HELPER_NAMES.has(declaration.name.text) ? declaration.name.text : null;
47757
+ };
47758
+ const mergeHelperStatement = (existingStatementText, helper) => {
47759
+ if (helper.strategy === "preserve-existing") return existingStatementText;
47760
+ if (helper.strategy === "replace") return helper.code.trim();
47761
+ const existingStatement = parseVariableStatement(existingStatementText);
47762
+ const nextStatement = parseVariableStatement(helper.code);
47763
+ if (!existingStatement || !nextStatement) return null;
47764
+ const existingInitializer = existingStatement.declaration.initializer;
47765
+ const newInitializer = nextStatement.declaration.initializer;
47766
+ if (!existingInitializer || !newInitializer || !ts.isObjectLiteralExpression(existingInitializer) || !ts.isObjectLiteralExpression(newInitializer)) return null;
47767
+ const mergedInitializer = mergeObjectLiteralText(existingInitializer.getText(existingStatement.sourceFile), newInitializer.getText(nextStatement.sourceFile));
47768
+ if (!mergedInitializer) return null;
47769
+ 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};`;
47770
+ };
47771
+ const updateManagedObject = (existingText, existingObject, newObject, existingSourceFile, newSourceFile) => {
47772
+ const objectStart = existingObject.getStart(existingSourceFile);
47773
+ const objectText = existingText.slice(objectStart, existingObject.end);
47774
+ const propertyEdits = [];
47775
+ const missingPropertyTexts = [];
47776
+ for (const propertyName of [
47777
+ "build",
47778
+ "storage",
47779
+ "database"
47780
+ ]) {
47781
+ const existingProperty = findManagedProperty(existingObject, propertyName);
47782
+ const nextProperty = findManagedProperty(newObject, propertyName);
47783
+ if (!nextProperty) continue;
47784
+ if (!existingProperty) {
47785
+ missingPropertyTexts.push(nextProperty.getText(newSourceFile));
47786
+ continue;
47787
+ }
47788
+ if (!ts.isCallExpression(existingProperty.initializer)) return null;
47789
+ if (!ts.isCallExpression(nextProperty.initializer)) return null;
47790
+ const existingCallee = getCallCallee(existingProperty.initializer);
47791
+ const nextCallee = getCallCallee(nextProperty.initializer);
47792
+ if (!existingCallee || !nextCallee) return null;
47793
+ let nextInitializerText = nextProperty.initializer.getText(newSourceFile);
47794
+ if (propertyName === "build") {
47795
+ if (existingCallee === nextCallee) continue;
47796
+ if (!KNOWN_BUILD_CALLEES.has(existingCallee)) return null;
47797
+ } else if (existingCallee === nextCallee) {
47798
+ const mergedInitializer = buildMergedCallInitializer(existingProperty.initializer, existingSourceFile, nextProperty.initializer, newSourceFile);
47799
+ if (mergedInitializer === null) return null;
47800
+ nextInitializerText = mergedInitializer;
47801
+ }
47802
+ propertyEdits.push({
47803
+ start: existingProperty.initializer.getStart(existingSourceFile) - objectStart,
47804
+ end: existingProperty.initializer.end - objectStart,
47805
+ text: nextInitializerText
47806
+ });
47807
+ }
47808
+ let mergedText = objectText;
47809
+ for (const edit of propertyEdits.sort((a, b) => b.start - a.start)) mergedText = mergedText.slice(0, edit.start) + edit.text + mergedText.slice(edit.end);
47810
+ return appendMissingProperties(mergedText, missingPropertyTexts, existingObject.properties.length > 0);
47811
+ };
47812
+ const rebuildImportBlock = (sourceText, sourceFile, scaffold) => {
47813
+ const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration);
47814
+ if (importDeclarations.length === 0) return {
47815
+ start: 0,
47816
+ end: 0,
47817
+ text: `${renderImportStatements(scaffold.imports)}\n\n`
47818
+ };
47819
+ const preservedImportTexts = importDeclarations.filter((declaration) => {
47820
+ const moduleSpecifier = declaration.moduleSpecifier;
47821
+ return ts.isStringLiteral(moduleSpecifier) && !MANAGED_IMPORT_PACKAGES.has(moduleSpecifier.text);
47822
+ }).map((declaration) => trimStatementText(sourceText.slice(declaration.getFullStart(), declaration.end)));
47823
+ const managedImportText = renderImportStatements(scaffold.imports);
47824
+ const nextImportBlock = [...preservedImportTexts, managedImportText].filter(Boolean).join("\n");
47825
+ return {
47826
+ start: importDeclarations[0].getFullStart(),
47827
+ end: importDeclarations.at(-1).end,
47828
+ text: `${nextImportBlock}\n\n`
47829
+ };
47830
+ };
47831
+ const rebuildManagedBody = (sourceText, sourceFile, exportAssignment, scaffold) => {
47832
+ const statementsBeforeExport = sourceFile.statements.filter((statement) => !ts.isImportDeclaration(statement) && statement.pos < exportAssignment.pos);
47833
+ const managedHelpers = new Map(scaffold.helperStatements.map((statement) => [statement.name, statement]));
47834
+ const emittedHelpers = /* @__PURE__ */ new Set();
47835
+ const bodyStatements = [];
47836
+ for (const statement of statementsBeforeExport) {
47837
+ if (isConfigCallStatement(statement)) continue;
47838
+ const helperName = isManagedHelperStatement(statement);
47839
+ if (!helperName) {
47840
+ bodyStatements.push(getStatementText(sourceText, statement));
47841
+ continue;
47842
+ }
47843
+ const helper = managedHelpers.get(helperName);
47844
+ if (!helper) continue;
47845
+ const mergedHelperStatement = mergeHelperStatement(getStatementText(sourceText, statement), helper);
47846
+ if (!mergedHelperStatement) return null;
47847
+ emittedHelpers.add(helperName);
47848
+ bodyStatements.push(mergedHelperStatement);
47849
+ }
47850
+ for (const helper of scaffold.helperStatements) if (!emittedHelpers.has(helper.name)) bodyStatements.push(helper.code.trim());
47851
+ const bodyText = bodyStatements.filter(Boolean).join("\n\n");
47852
+ const configStatement = `config({ path: ".env.hotupdater" });`;
47853
+ const managedBody = bodyText ? `\n\n${configStatement}\n\n${bodyText}\n\n` : `\n\n${configStatement}\n\n`;
47854
+ return {
47855
+ start: sourceFile.statements.filter(ts.isImportDeclaration).at(-1)?.end ?? 0,
47856
+ end: exportAssignment.getFullStart(),
47857
+ text: managedBody
47858
+ };
47859
+ };
47860
+ const createHotUpdaterConfigScaffold = ({ build, storage, database, extraImports = [], helperStatements = [], updateStrategy = "appVersion" }) => {
47861
+ const intermediateCode = helperStatements.map((statement) => statement.code.trim()).filter(Boolean).join("\n\n");
47862
+ const builder = new ConfigBuilder().setBuildType(build).setStorage(storage).setDatabase(database);
47863
+ for (const extraImport of extraImports) builder.addImport(extraImport);
47864
+ if (intermediateCode) builder.setIntermediateCode(intermediateCode);
47865
+ return createHotUpdaterConfigScaffoldFromBuilder(builder, {
47866
+ helperStatements,
47867
+ updateStrategy
47868
+ });
47869
+ };
47870
+ const createHotUpdaterConfigScaffoldFromBuilder = (builder, { helperStatements = [], updateStrategy = "appVersion" } = {}) => {
47871
+ const scaffold = builder.getScaffold();
47872
+ return {
47873
+ text: updateStrategy === "appVersion" ? scaffold.text : scaffold.text.replace("updateStrategy: \"appVersion\"", `updateStrategy: "${updateStrategy}"`),
47874
+ imports: scaffold.imports,
47875
+ build: {
47876
+ initializer: scaffold.buildConfigString,
47877
+ callee: extractCallIdentifier(scaffold.buildConfigString)
47878
+ },
47879
+ storage: {
47880
+ initializer: scaffold.storageConfigString,
47881
+ callee: extractCallIdentifier(scaffold.storageConfigString)
47882
+ },
47883
+ database: {
47884
+ initializer: scaffold.databaseConfigString,
47885
+ callee: extractCallIdentifier(scaffold.databaseConfigString)
47886
+ },
47887
+ helperStatements,
47888
+ updateStrategy: `"${updateStrategy}"`
47889
+ };
47890
+ };
47891
+ const writeHotUpdaterConfig = async (scaffold, filePath = HOT_UPDATER_CONFIG_PATH) => {
47892
+ const existingText = await fs$1.readFile(filePath, "utf-8").catch((error) => {
47893
+ if (error.code === "ENOENT") return null;
47894
+ throw error;
47895
+ });
47896
+ if (existingText === null) {
47897
+ await fs$1.writeFile(filePath, `${scaffold.text}\n`, "utf-8");
47898
+ return {
47899
+ status: "created",
47900
+ path: filePath
47901
+ };
47902
+ }
47903
+ const existingSourceFile = createSnippetSourceFile(existingText);
47904
+ const existingConfig = findDefineConfigObject(existingSourceFile);
47905
+ const nextSourceFile = createSnippetSourceFile(scaffold.text);
47906
+ const nextConfig = findDefineConfigObject(nextSourceFile);
47907
+ if (!existingConfig || !nextConfig) return {
47908
+ status: "skipped",
47909
+ path: filePath,
47910
+ reason: "Existing config is not a supported `export default defineConfig({ ... })` shape."
47911
+ };
47912
+ const nextObjectText = updateManagedObject(existingText, existingConfig.objectLiteral, nextConfig.objectLiteral, existingSourceFile, nextSourceFile);
47913
+ if (!nextObjectText) return {
47914
+ status: "skipped",
47915
+ path: filePath,
47916
+ reason: "Existing config uses dynamic build/storage/database expressions that cannot be merged safely."
47917
+ };
47918
+ const objectEdit = {
47919
+ start: existingConfig.objectLiteral.getStart(existingSourceFile),
47920
+ end: existingConfig.objectLiteral.end,
47921
+ text: nextObjectText
47922
+ };
47923
+ const importEdit = rebuildImportBlock(existingText, existingSourceFile, scaffold);
47924
+ const bodyEdit = rebuildManagedBody(existingText, existingSourceFile, existingConfig.exportAssignment, scaffold);
47925
+ if (!bodyEdit) return {
47926
+ status: "skipped",
47927
+ path: filePath,
47928
+ reason: "Existing helper declarations could not be merged safely."
47929
+ };
47930
+ let mergedText = existingText;
47931
+ for (const edit of [
47932
+ objectEdit,
47933
+ bodyEdit,
47934
+ importEdit
47935
+ ].sort((a, b) => b.start - a.start)) mergedText = mergedText.slice(0, edit.start) + edit.text + mergedText.slice(edit.end);
47936
+ await fs$1.writeFile(filePath, mergedText, "utf-8");
47937
+ return {
47938
+ status: "merged",
47939
+ path: filePath
47940
+ };
47941
+ };
47942
+ //#endregion
47541
47943
  //#region ../../node_modules/.pnpm/es-toolkit@1.32.0/node_modules/es-toolkit/dist/predicate/isPlainObject.mjs
47542
47944
  function isPlainObject(value) {
47543
47945
  if (!value || typeof value !== "object") return false;
@@ -47659,8 +48061,9 @@ const log = {
47659
48061
  };
47660
48062
  //#endregion
47661
48063
  //#region src/makeEnv.ts
47662
- const makeEnv = async (newEnvVars, filePath = ".env.hotupdater") => {
48064
+ const makeEnv = async (newEnvVars, filePath = ".env.hotupdater", options) => {
47663
48065
  try {
48066
+ const preserveKeys = new Set(options?.preserveKeys ?? []);
47664
48067
  const existingContent = await fs$1.readFile(filePath, "utf-8").catch(() => "");
47665
48068
  const lines = existingContent ? existingContent.split("\n") : [];
47666
48069
  const processedKeys = /* @__PURE__ */ new Set();
@@ -47677,7 +48080,7 @@ const makeEnv = async (newEnvVars, filePath = ".env.hotupdater") => {
47677
48080
  const nextLine = (lines[i + 1] ?? "").trim();
47678
48081
  if (nextLine && !nextLine.startsWith("#") && nextLine.includes("=")) {
47679
48082
  const [possibleKey = ""] = nextLine.split("=");
47680
- if (Object.hasOwn(newEnvVars, possibleKey.trim())) continue;
48083
+ if (Object.hasOwn(newEnvVars, possibleKey.trim()) && !preserveKeys.has(possibleKey.trim())) continue;
47681
48084
  }
47682
48085
  }
47683
48086
  updatedLines.push(line);
@@ -47688,6 +48091,10 @@ const makeEnv = async (newEnvVars, filePath = ".env.hotupdater") => {
47688
48091
  const key = keyPart?.trim() ?? "";
47689
48092
  if (Object.hasOwn(newEnvVars, key)) {
47690
48093
  processedKeys.add(key);
48094
+ if (preserveKeys.has(key)) {
48095
+ updatedLines.push(line);
48096
+ continue;
48097
+ }
47691
48098
  const newValue = newEnvVars[key];
47692
48099
  if (typeof newValue === "object" && newValue !== null) {
47693
48100
  updatedLines.push(`# ${newValue.comment}`);
@@ -47775,4 +48182,4 @@ function transformTemplate(templateString, values) {
47775
48182
  }
47776
48183
  //#endregion
47777
48184
  var colors = import_picocolors.default;
47778
- export { BuildLogger, ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, banner, colors, copyDirToTmp, createLogWriter, createTarBr, createTarBrTargetFiles, createTarGz, createTarGzTargetFiles, createZip, createZipTargetFiles, decryptJson, encryptJson, ensureInstallPackages, getAndroidSdkPath, getCwd, getPackageManager, getReactNativeMetadatas, link, loadConfig, log, makeEnv, dist_exports as p, printBanner, resolveHotUpdaterServerVersion, resolvePackageVersion, stripAnsi, transformEnv, transformTemplate };
48185
+ export { BuildLogger, ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, banner, colors, copyDirToTmp, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBr, createTarBrTargetFiles, createTarGz, createTarGzTargetFiles, createZip, createZipTargetFiles, decryptJson, encryptJson, ensureInstallPackages, getAndroidSdkPath, getCwd, getPackageManager, getReactNativeMetadatas, link, loadConfig, log, makeEnv, dist_exports as p, printBanner, renderImportStatements, resolveHotUpdaterServerVersion, resolvePackageVersion, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/cli-tools",
3
- "version": "0.29.5",
3
+ "version": "0.29.6",
4
4
  "type": "module",
5
5
  "description": "CLI utilities for Hot Updater",
6
6
  "sideEffects": false,
@@ -40,8 +40,9 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "oxc-transform": "0.121.0",
43
+ "typescript": "6.0.2",
43
44
  "unconfig": "7.5.0",
44
- "@hot-updater/plugin-core": "0.29.5"
45
+ "@hot-updater/plugin-core": "0.29.6"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@clack/prompts": "1.0.1",
@@ -56,9 +57,8 @@
56
57
  "read-package-up": "^11.0.0",
57
58
  "semver": "^7.6.3",
58
59
  "tar": "^7.5.1",
59
- "typescript": "6.0.2",
60
60
  "workspace-tools": "^0.36.4",
61
- "@hot-updater/test-utils": "0.29.5"
61
+ "@hot-updater/test-utils": "0.29.6"
62
62
  },
63
63
  "inlinedDependencies": {
64
64
  "@babel/code-frame": "7.29.0",