hot-updater 0.31.0 → 0.31.1

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.
Files changed (2) hide show
  1. package/dist/index.mjs +303 -4
  2. package/package.json +14 -14
package/dist/index.mjs CHANGED
@@ -49715,6 +49715,9 @@ const INFRASTRUCTURE_RECOVERY_COMMANDS = [
49715
49715
  "hot-updater db migrate",
49716
49716
  "hot-updater db generate"
49717
49717
  ];
49718
+ const FINGERPRINT_RECOVERY_COMMANDS = ["npx hot-updater fingerprint create"];
49719
+ const EXPORT_PUBLIC_KEY_COMMANDS = ["npx hot-updater keys export-public"];
49720
+ const REMOVE_PUBLIC_KEY_COMMANDS = ["npx hot-updater keys remove"];
49718
49721
  const INFRASTRUCTURE_UPDATE_TARGETS = [
49719
49722
  {
49720
49723
  version: "0.13.0",
@@ -49780,7 +49783,267 @@ function resolveVersionEndpoint(serverBaseUrl) {
49780
49783
  url.pathname = `${pathname}/version`;
49781
49784
  return url.toString();
49782
49785
  }
49783
- const createInfrastructureRemediation = () => ({ commands: [...INFRASTRUCTURE_RECOVERY_COMMANDS] });
49786
+ const createInfrastructureRemediation = () => ({
49787
+ fixability: "blocked",
49788
+ reason: "Server infrastructure changes usually need provider credentials, environment variables, and redeploy access.",
49789
+ commands: [...INFRASTRUCTURE_RECOVERY_COMMANDS]
49790
+ });
49791
+ const toRelativePath = (cwd, filePath) => path$1.relative(cwd, filePath);
49792
+ const resolveProjectPath = (cwd, filePath) => path$1.isAbsolute(filePath) ? filePath : path$1.join(cwd, filePath);
49793
+ const findNativeFiles = ({ cwd, platform, pattern }) => {
49794
+ const platformRoot = path$1.join(cwd, platform);
49795
+ if (!fs$2.existsSync(platformRoot)) return [];
49796
+ return import_out.default.sync(pattern, {
49797
+ cwd: platformRoot,
49798
+ absolute: true,
49799
+ onlyFiles: true,
49800
+ ignore: [
49801
+ "**/Pods/**",
49802
+ "**/build/**",
49803
+ "**/Build/**",
49804
+ "**/*.app/**",
49805
+ "**/*.xcarchive/**"
49806
+ ]
49807
+ }).map((filePath) => toRelativePath(cwd, filePath)).sort();
49808
+ };
49809
+ const findFirstMatchingFile = async ({ cwd, files, patterns }) => {
49810
+ for (const filePath of files) {
49811
+ const absolutePath = resolveProjectPath(cwd, filePath);
49812
+ const content = await fs$2.promises.readFile(absolutePath, "utf-8");
49813
+ if (patterns.some((pattern) => pattern.test(content))) return filePath;
49814
+ }
49815
+ return null;
49816
+ };
49817
+ const readLocalFingerprintFile = async (cwd) => {
49818
+ const fingerprintJsonPath = path$1.join(cwd, "fingerprint.json");
49819
+ try {
49820
+ const content = await fs$2.promises.readFile(fingerprintJsonPath, "utf-8");
49821
+ return {
49822
+ path: "fingerprint.json",
49823
+ value: JSON.parse(content)
49824
+ };
49825
+ } catch {
49826
+ return null;
49827
+ }
49828
+ };
49829
+ const checkIosNativeStatus = async ({ cwd, config, requireFingerprint, expectedFingerprintHash }) => {
49830
+ const configuredPaths = config.platform.ios.infoPlistPaths;
49831
+ if (!(fs$2.existsSync(path$1.join(cwd, "ios")) || configuredPaths.length > 0)) return { issues: [] };
49832
+ const iosParser = new IosConfigParser(configuredPaths);
49833
+ const files = configuredPaths.filter((filePath) => fs$2.existsSync(resolveProjectPath(cwd, filePath)));
49834
+ const issues = [];
49835
+ if (!await iosParser.exists()) issues.push({
49836
+ type: "error",
49837
+ platform: "ios",
49838
+ code: "NATIVE_FILES_NOT_FOUND",
49839
+ message: "iOS Info.plist files were not found.",
49840
+ resolution: "Check platform.ios.infoPlistPaths in hot-updater.config.ts or run iOS prebuild first.",
49841
+ fixability: "auto",
49842
+ paths: configuredPaths
49843
+ });
49844
+ const channel = await iosParser.get("HOT_UPDATER_CHANNEL");
49845
+ const fingerprintHash = requireFingerprint ? await iosParser.get("HOT_UPDATER_FINGERPRINT_HASH") : void 0;
49846
+ if (requireFingerprint && !fingerprintHash?.value) issues.push({
49847
+ type: "error",
49848
+ platform: "ios",
49849
+ code: "MISSING_FINGERPRINT_HASH",
49850
+ message: "HOT_UPDATER_FINGERPRINT_HASH is missing from Info.plist.",
49851
+ resolution: "Run `npx hot-updater fingerprint create` or rebuild through the Expo config plugin.",
49852
+ fixability: "command",
49853
+ commands: [...FINGERPRINT_RECOVERY_COMMANDS],
49854
+ paths: fingerprintHash?.paths.length ? fingerprintHash.paths : files
49855
+ });
49856
+ else if (requireFingerprint && expectedFingerprintHash && fingerprintHash?.value !== expectedFingerprintHash) issues.push({
49857
+ type: "error",
49858
+ platform: "ios",
49859
+ code: "FINGERPRINT_HASH_MISMATCH",
49860
+ message: "HOT_UPDATER_FINGERPRINT_HASH does not match fingerprint.json.",
49861
+ resolution: "Run `npx hot-updater fingerprint create` and rebuild your iOS app.",
49862
+ fixability: "command",
49863
+ commands: [...FINGERPRINT_RECOVERY_COMMANDS],
49864
+ paths: fingerprintHash?.paths ?? files
49865
+ });
49866
+ const appDelegateFiles = findNativeFiles({
49867
+ cwd,
49868
+ platform: "ios",
49869
+ pattern: "**/AppDelegate.{swift,mm,m}"
49870
+ });
49871
+ let bundleProviderConfigured = false;
49872
+ if (appDelegateFiles.length === 0) issues.push({
49873
+ type: "error",
49874
+ platform: "ios",
49875
+ code: "APP_DELEGATE_NOT_FOUND",
49876
+ message: "iOS AppDelegate file was not found.",
49877
+ resolution: "Add HotUpdater.bundleURL() to the app's iOS bundleURL provider.",
49878
+ fixability: "auto"
49879
+ });
49880
+ else {
49881
+ bundleProviderConfigured = await findFirstMatchingFile({
49882
+ cwd,
49883
+ files: appDelegateFiles,
49884
+ patterns: [/HotUpdater\.bundleURL\s*\(/, /\[HotUpdater\s+bundleURL(?:WithBundle)?:?/]
49885
+ }) !== null;
49886
+ if (!bundleProviderConfigured) issues.push({
49887
+ type: "error",
49888
+ platform: "ios",
49889
+ code: "MISSING_IOS_BUNDLE_PROVIDER",
49890
+ message: "iOS AppDelegate does not use HotUpdater.bundleURL().",
49891
+ resolution: "Replace the release JS bundle URL provider with HotUpdater.bundleURL().",
49892
+ fixability: "auto",
49893
+ paths: appDelegateFiles
49894
+ });
49895
+ }
49896
+ return {
49897
+ status: {
49898
+ detected: true,
49899
+ files: [...files, ...appDelegateFiles],
49900
+ channel: channel.value ?? void 0,
49901
+ fingerprintHash: fingerprintHash?.value ?? void 0,
49902
+ bundleProviderConfigured
49903
+ },
49904
+ issues
49905
+ };
49906
+ };
49907
+ const checkAndroidNativeStatus = async ({ cwd, config, requireFingerprint, expectedFingerprintHash }) => {
49908
+ const configuredPaths = config.platform.android.stringResourcePaths;
49909
+ if (!(fs$2.existsSync(path$1.join(cwd, "android")) || configuredPaths.length > 0)) return { issues: [] };
49910
+ const androidParser = new AndroidConfigParser(configuredPaths);
49911
+ const files = configuredPaths.filter((filePath) => fs$2.existsSync(resolveProjectPath(cwd, filePath)));
49912
+ const issues = [];
49913
+ if (!await androidParser.exists()) issues.push({
49914
+ type: "error",
49915
+ platform: "android",
49916
+ code: "NATIVE_FILES_NOT_FOUND",
49917
+ message: "Android strings.xml files were not found.",
49918
+ resolution: "Check platform.android.stringResourcePaths in hot-updater.config.ts or run Android prebuild first.",
49919
+ fixability: "auto",
49920
+ paths: configuredPaths
49921
+ });
49922
+ const channel = await androidParser.get("hot_updater_channel");
49923
+ const fingerprintHash = requireFingerprint ? await androidParser.get("hot_updater_fingerprint_hash") : void 0;
49924
+ if (requireFingerprint && !fingerprintHash?.value) issues.push({
49925
+ type: "error",
49926
+ platform: "android",
49927
+ code: "MISSING_FINGERPRINT_HASH",
49928
+ message: "hot_updater_fingerprint_hash is missing from strings.xml.",
49929
+ resolution: "Run `npx hot-updater fingerprint create` or rebuild through the Expo config plugin.",
49930
+ fixability: "command",
49931
+ commands: [...FINGERPRINT_RECOVERY_COMMANDS],
49932
+ paths: fingerprintHash?.paths.length ? fingerprintHash.paths : files
49933
+ });
49934
+ else if (requireFingerprint && expectedFingerprintHash && fingerprintHash?.value !== expectedFingerprintHash) issues.push({
49935
+ type: "error",
49936
+ platform: "android",
49937
+ code: "FINGERPRINT_HASH_MISMATCH",
49938
+ message: "hot_updater_fingerprint_hash does not match fingerprint.json.",
49939
+ resolution: "Run `npx hot-updater fingerprint create` and rebuild your Android app.",
49940
+ fixability: "command",
49941
+ commands: [...FINGERPRINT_RECOVERY_COMMANDS],
49942
+ paths: fingerprintHash?.paths ?? files
49943
+ });
49944
+ const mainApplicationFiles = findNativeFiles({
49945
+ cwd,
49946
+ platform: "android",
49947
+ pattern: "**/MainApplication.{kt,java}"
49948
+ });
49949
+ let bundleProviderConfigured = false;
49950
+ if (mainApplicationFiles.length === 0) issues.push({
49951
+ type: "error",
49952
+ platform: "android",
49953
+ code: "MAIN_APPLICATION_NOT_FOUND",
49954
+ message: "Android MainApplication file was not found.",
49955
+ resolution: "Add HotUpdater.getJSBundleFile(applicationContext) to the Android host configuration.",
49956
+ fixability: "auto"
49957
+ });
49958
+ else {
49959
+ bundleProviderConfigured = await findFirstMatchingFile({
49960
+ cwd,
49961
+ files: mainApplicationFiles,
49962
+ patterns: [/HotUpdater\s*(?:\.\s*Companion\s*)?\.\s*getJSBundleFile\s*\(/]
49963
+ }) !== null;
49964
+ if (!bundleProviderConfigured) issues.push({
49965
+ type: "error",
49966
+ platform: "android",
49967
+ code: "MISSING_ANDROID_BUNDLE_PROVIDER",
49968
+ message: "Android MainApplication does not use HotUpdater.getJSBundleFile().",
49969
+ resolution: "Pass HotUpdater.getJSBundleFile(applicationContext) to React Native's JS bundle provider.",
49970
+ fixability: "auto",
49971
+ paths: mainApplicationFiles
49972
+ });
49973
+ }
49974
+ return {
49975
+ status: {
49976
+ detected: true,
49977
+ files: [...files, ...mainApplicationFiles],
49978
+ channel: channel.value ?? void 0,
49979
+ fingerprintHash: fingerprintHash?.value ?? void 0,
49980
+ bundleProviderConfigured
49981
+ },
49982
+ issues
49983
+ };
49984
+ };
49985
+ const toNativeIssue = (issue) => {
49986
+ if (issue.code === "NATIVE_FILES_NOT_FOUND") return {
49987
+ type: issue.type,
49988
+ platform: issue.platform,
49989
+ code: issue.code,
49990
+ message: issue.message,
49991
+ resolution: issue.resolution,
49992
+ fixability: "auto"
49993
+ };
49994
+ return {
49995
+ type: issue.type,
49996
+ platform: issue.platform,
49997
+ code: issue.code,
49998
+ message: issue.message,
49999
+ resolution: issue.resolution,
50000
+ fixability: "command",
50001
+ commands: issue.code === "ORPHAN_PUBLIC_KEY" ? [...REMOVE_PUBLIC_KEY_COMMANDS] : [...EXPORT_PUBLIC_KEY_COMMANDS]
50002
+ };
50003
+ };
50004
+ async function checkNativeStatus({ cwd }) {
50005
+ if (!(fs$2.existsSync(path$1.join(cwd, "ios")) || fs$2.existsSync(path$1.join(cwd, "android")))) return;
50006
+ const config = await loadConfig(null);
50007
+ const localFingerprint = await readLocalFingerprintFile(cwd);
50008
+ const requireFingerprint = config.updateStrategy === "fingerprint";
50009
+ const [ios, android, signing] = await Promise.all([
50010
+ checkIosNativeStatus({
50011
+ cwd,
50012
+ config,
50013
+ requireFingerprint,
50014
+ expectedFingerprintHash: localFingerprint?.value.ios?.hash
50015
+ }),
50016
+ checkAndroidNativeStatus({
50017
+ cwd,
50018
+ config,
50019
+ requireFingerprint,
50020
+ expectedFingerprintHash: localFingerprint?.value.android?.hash
50021
+ }),
50022
+ validateSigningConfig(config)
50023
+ ]);
50024
+ const issues = [
50025
+ ...ios.issues,
50026
+ ...android.issues,
50027
+ ...signing.issues.map(toNativeIssue)
50028
+ ];
50029
+ if (requireFingerprint && !localFingerprint) issues.push({
50030
+ type: "error",
50031
+ platform: "project",
50032
+ code: "MISSING_FINGERPRINT_JSON",
50033
+ message: "fingerprint.json is missing for fingerprint update strategy.",
50034
+ resolution: "Run `npx hot-updater fingerprint create`.",
50035
+ fixability: "command",
50036
+ commands: [...FINGERPRINT_RECOVERY_COMMANDS],
50037
+ paths: ["fingerprint.json"]
50038
+ });
50039
+ return {
50040
+ updateStrategy: config.updateStrategy,
50041
+ fingerprintJsonPath: localFingerprint?.path,
50042
+ ios: ios.status,
50043
+ android: android.status,
50044
+ issues
50045
+ };
50046
+ }
49784
50047
  async function checkInfrastructureStatus({ serverBaseUrl, fetchImpl = fetch, requiredVersion = getRequiredInfrastructureVersion() }) {
49785
50048
  const versionEndpoint = resolveVersionEndpoint(serverBaseUrl);
49786
50049
  const baseUrl = serverBaseUrl.trim();
@@ -49850,6 +50113,7 @@ async function doctor(options = {}) {
49850
50113
  error: "hot-updater CLI not found. Please install it first."
49851
50114
  };
49852
50115
  const hotUpdaterPackages = Object.keys(allDependencies).filter((key) => key.startsWith("@hot-updater/"));
50116
+ const hasReactNativePackage = allDependencies["@hot-updater/react-native"] !== void 0;
49853
50117
  const versionMismatches = [];
49854
50118
  for (const packageName of hotUpdaterPackages) {
49855
50119
  const currentVersion = allDependencies[packageName];
@@ -49872,9 +50136,11 @@ async function doctor(options = {}) {
49872
50136
  });
49873
50137
  if (details.infrastructure.error !== void 0 || details.infrastructure.needsUpdate === true) details.infrastructure.remediation = createInfrastructureRemediation();
49874
50138
  }
50139
+ if (hasReactNativePackage) details.native = await checkNativeStatus({ cwd });
49875
50140
  if (versionMismatches.length > 0) details.versionMismatches = versionMismatches;
49876
50141
  const hasInfrastructureIssue = details.infrastructure?.error !== void 0 || details.infrastructure?.needsUpdate === true;
49877
- if (versionMismatches.length > 0 || hasInfrastructureIssue) return {
50142
+ const hasNativeIssue = details.native?.issues.some((issue) => issue.type === "error") === true;
50143
+ if (versionMismatches.length > 0 || hasInfrastructureIssue || hasNativeIssue) return {
49878
50144
  success: false,
49879
50145
  details
49880
50146
  };
@@ -49882,6 +50148,10 @@ async function doctor(options = {}) {
49882
50148
  success: true,
49883
50149
  details
49884
50150
  };
50151
+ if (details.native) return {
50152
+ success: true,
50153
+ details
50154
+ };
49885
50155
  return true;
49886
50156
  } catch (error) {
49887
50157
  return {
@@ -49890,6 +50160,10 @@ async function doctor(options = {}) {
49890
50160
  };
49891
50161
  }
49892
50162
  }
50163
+ const normalizeDoctorResult = (result) => {
50164
+ if (result === true) return { success: true };
50165
+ return result;
50166
+ };
49893
50167
  const promptServerBaseUrl = async () => {
49894
50168
  if (!process.stdin.isTTY || !process.stdout.isTTY) return;
49895
50169
  const serverBaseUrl = await p.text({
@@ -49912,7 +50186,13 @@ const promptServerBaseUrl = async () => {
49912
50186
  const trimmed = serverBaseUrl.trim();
49913
50187
  return trimmed ? trimmed : void 0;
49914
50188
  };
49915
- const handleDoctor = async ({ serverBaseUrl } = {}) => {
50189
+ const handleDoctor = async ({ serverBaseUrl, json = false } = {}) => {
50190
+ if (json) {
50191
+ const result = normalizeDoctorResult(await doctor({ serverBaseUrl }));
50192
+ console.log(JSON.stringify(result, null, 2));
50193
+ if (!result.success) process.exit(1);
50194
+ return;
50195
+ }
49916
50196
  p.intro("Hot Updater doctor");
49917
50197
  const result = await doctor({ serverBaseUrl: serverBaseUrl ?? await promptServerBaseUrl() });
49918
50198
  if (result === true) {
@@ -49948,6 +50228,25 @@ const handleDoctor = async ({ serverBaseUrl } = {}) => {
49948
50228
  "then redeploy server"
49949
50229
  ]))]));
49950
50230
  }
50231
+ if (details?.native) {
50232
+ const native = details.native;
50233
+ const lines = [ui.kv("Strategy", native.updateStrategy)];
50234
+ if (native.ios?.detected) {
50235
+ lines.push(ui.kv("iOS", native.ios.bundleProviderConfigured ? ui.status(true) : ui.status(false)));
50236
+ if (native.ios.channel) lines.push(ui.kv("iOS channel", ui.channel(native.ios.channel)));
50237
+ }
50238
+ if (native.android?.detected) {
50239
+ lines.push(ui.kv("Android", native.android.bundleProviderConfigured ? ui.status(true) : ui.status(false)));
50240
+ if (native.android.channel) lines.push(ui.kv("Android channel", ui.channel(native.android.channel)));
50241
+ }
50242
+ p.log.message(ui.block("Native", lines));
50243
+ for (const issue of native.issues) {
50244
+ const message = `${issue.platform}: ${issue.message}`;
50245
+ if (issue.type === "error") p.log.error(message);
50246
+ else p.log.warn(message);
50247
+ p.log.info(issue.resolution);
50248
+ }
50249
+ }
49951
50250
  if (details?.versionMismatches && details.versionMismatches.length > 0) {
49952
50251
  p.log.warn("Version mismatches found:");
49953
50252
  for (const mismatch of details.versionMismatches) p.log.error(`${mismatch.packageName}: ${mismatch.currentVersion} (expected ${mismatch.expectedVersion})`);
@@ -51250,7 +51549,7 @@ const parseRolloutCohortCount = (value) => {
51250
51549
  const program = new Command();
51251
51550
  program.name("hot-updater").description(banner(version)).version(version);
51252
51551
  program.command("init").description("Initialize Hot Updater").action(init);
51253
- program.command("doctor").description("Check the health of Hot Updater").option("--server-base-url <url>", "server base URL used by update checks (doctor appends /version)").action(handleDoctor);
51552
+ program.command("doctor").description("Check the health of Hot Updater").option("--server-base-url <url>", "server base URL used by update checks (doctor appends /version)").option("--json", "output machine-readable doctor result").action(handleDoctor);
51254
51553
  const fingerprintCommand = program.command("fingerprint").description("Generate fingerprint");
51255
51554
  fingerprintCommand.action(handleFingerprint);
51256
51555
  fingerprintCommand.command("create").description("Create fingerprint").action(handleCreateFingerprint);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hot-updater",
3
3
  "type": "module",
4
- "version": "0.31.0",
4
+ "version": "0.31.1",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
7
7
  },
@@ -49,13 +49,13 @@
49
49
  "jiti": "2.6.1",
50
50
  "kysely": "0.28.16",
51
51
  "sql-formatter": "15.6.10",
52
- "@hot-updater/cli-tools": "0.31.0",
53
- "@hot-updater/android-helper": "0.31.0",
54
- "@hot-updater/console": "0.31.0",
55
- "@hot-updater/core": "0.31.0",
56
- "@hot-updater/plugin-core": "0.31.0",
57
- "@hot-updater/apple-helper": "0.31.0",
58
- "@hot-updater/server": "0.31.0"
52
+ "@hot-updater/android-helper": "0.31.1",
53
+ "@hot-updater/apple-helper": "0.31.1",
54
+ "@hot-updater/cli-tools": "0.31.1",
55
+ "@hot-updater/plugin-core": "0.31.1",
56
+ "@hot-updater/core": "0.31.1",
57
+ "@hot-updater/console": "0.31.1",
58
+ "@hot-updater/server": "0.31.1"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@bacons/xcode": "1.0.0-alpha.24",
@@ -74,12 +74,12 @@
74
74
  "plist": "^3.1.0",
75
75
  "semver": "^7.6.3",
76
76
  "typescript": "6.0.2",
77
- "@hot-updater/aws": "0.31.0",
78
- "@hot-updater/cloudflare": "0.31.0",
79
- "@hot-updater/firebase": "0.31.0",
80
- "@hot-updater/server": "0.31.0",
81
- "@hot-updater/supabase": "0.31.0",
82
- "@hot-updater/test-utils": "0.31.0"
77
+ "@hot-updater/aws": "0.31.1",
78
+ "@hot-updater/cloudflare": "0.31.1",
79
+ "@hot-updater/firebase": "0.31.1",
80
+ "@hot-updater/server": "0.31.1",
81
+ "@hot-updater/supabase": "0.31.1",
82
+ "@hot-updater/test-utils": "0.31.1"
83
83
  },
84
84
  "peerDependencies": {
85
85
  "@hot-updater/aws": "*",