hot-updater 1.0.0 → 1.0.2

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 (68) hide show
  1. package/dist/index.mjs +266 -77
  2. package/dist/infra-templates/aws/COMMON.md +3 -1
  3. package/dist/infra-templates/aws/app/provision-client-credential.mjs +5 -3
  4. package/dist/infra-templates/aws/lambda/index.cjs +1 -1
  5. package/dist/infra-templates/aws/template.json +10 -10
  6. package/dist/infra-templates/aws/upgrades/README.md +4 -0
  7. package/dist/infra-templates/azure/COMMON.md +310 -0
  8. package/dist/infra-templates/azure/ENVIRONMENT.md +35 -0
  9. package/dist/infra-templates/azure/SETUP.md +163 -0
  10. package/dist/infra-templates/azure/app/hot-updater.config.bare.ts +22 -0
  11. package/dist/infra-templates/azure/app/hot-updater.config.expo.ts +22 -0
  12. package/dist/infra-templates/azure/app/hot-updater.config.rock.ts +22 -0
  13. package/dist/infra-templates/azure/app/hotUpdater.ts +20 -0
  14. package/dist/infra-templates/azure/app/migrate.ts +17 -0
  15. package/dist/infra-templates/azure/app/provision-client-credential.mjs +89 -0
  16. package/dist/infra-templates/azure/app/verify-server.mjs +381 -0
  17. package/dist/infra-templates/azure/azure/function/host.json +8 -0
  18. package/dist/infra-templates/azure/azure/function/index.mjs +70656 -0
  19. package/dist/infra-templates/azure/azure/function/package.json +6 -0
  20. package/dist/infra-templates/azure/template.json +83 -0
  21. package/dist/infra-templates/azure/upgrades/1.0.0.md +215 -0
  22. package/dist/infra-templates/azure/upgrades/README.md +13 -0
  23. package/dist/infra-templates/cloudflare/COMMON.md +3 -1
  24. package/dist/infra-templates/cloudflare/app/provision-client-credential.mjs +5 -3
  25. package/dist/infra-templates/cloudflare/template.json +10 -10
  26. package/dist/infra-templates/cloudflare/upgrades/README.md +4 -0
  27. package/dist/infra-templates/cloudflare/worker/dist/README.md +1 -1
  28. package/dist/infra-templates/cloudflare/worker/dist/index.js +1 -1
  29. package/dist/infra-templates/cloudflare/worker/dist/index.js.map +2 -2
  30. package/dist/infra-templates/firebase/COMMON.md +3 -1
  31. package/dist/infra-templates/firebase/ENVIRONMENT.md +2 -1
  32. package/dist/infra-templates/firebase/SETUP.md +19 -8
  33. package/dist/infra-templates/firebase/app/hot-updater.config.bare.ts +1 -0
  34. package/dist/infra-templates/firebase/app/hot-updater.config.expo.ts +1 -0
  35. package/dist/infra-templates/firebase/app/hot-updater.config.rock.ts +1 -0
  36. package/dist/infra-templates/firebase/app/hotUpdater.ts +1 -0
  37. package/dist/infra-templates/firebase/app/migrate.ts +3 -2
  38. package/dist/infra-templates/firebase/app/provision-client-credential.mjs +5 -3
  39. package/dist/infra-templates/firebase/firebase/firebase.json +2 -1
  40. package/dist/infra-templates/firebase/firebase/functions/index.cjs +8 -4
  41. package/dist/infra-templates/firebase/template.json +17 -11
  42. package/dist/infra-templates/firebase/upgrades/README.md +4 -0
  43. package/dist/infra-templates/supabase/COMMON.md +3 -1
  44. package/dist/infra-templates/supabase/app/provision-client-credential.mjs +5 -3
  45. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-protocol/dist/index.cjs +5 -0
  46. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-protocol/dist/index.d.cts +14 -0
  47. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-protocol/dist/index.d.mts +14 -0
  48. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-protocol/dist/index.mjs +5 -0
  49. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/package.cjs +1 -1
  50. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/package.mjs +1 -1
  51. package/dist/infra-templates/supabase/template.json +10 -10
  52. package/dist/infra-templates/supabase/upgrades/README.md +4 -0
  53. package/dist/infra-templates/vercel/COMMON.md +310 -0
  54. package/dist/infra-templates/vercel/ENVIRONMENT.md +33 -0
  55. package/dist/infra-templates/vercel/SETUP.md +98 -0
  56. package/dist/infra-templates/vercel/app/hot-updater.config.bare.ts +26 -0
  57. package/dist/infra-templates/vercel/app/hot-updater.config.expo.ts +26 -0
  58. package/dist/infra-templates/vercel/app/hot-updater.config.rock.ts +26 -0
  59. package/dist/infra-templates/vercel/app/hotUpdater.ts +24 -0
  60. package/dist/infra-templates/vercel/app/migrate.ts +17 -0
  61. package/dist/infra-templates/vercel/app/provision-client-credential.mjs +89 -0
  62. package/dist/infra-templates/vercel/app/verify-server.mjs +381 -0
  63. package/dist/infra-templates/vercel/template.json +89 -0
  64. package/dist/infra-templates/vercel/upgrades/1.0.0.md +215 -0
  65. package/dist/infra-templates/vercel/upgrades/README.md +13 -0
  66. package/dist/infra-templates/vercel/vercel/api/index.mjs +53589 -0
  67. package/dist/infra-templates/vercel/vercel/vercel.json +14 -0
  68. package/package.json +30 -20
package/dist/index.mjs CHANGED
@@ -3790,6 +3790,18 @@ const INIT_PROVIDER_PACKAGES = {
3790
3790
  label: "Firebase",
3791
3791
  load: () => import("@hot-updater/firebase/init"),
3792
3792
  packageName: "@hot-updater/firebase"
3793
+ },
3794
+ vercel: {
3795
+ devDependencies: ["vercel", "@hot-updater/postgres"],
3796
+ label: "Vercel Blob + Functions + Postgres",
3797
+ load: () => import("@hot-updater/vercel/init"),
3798
+ packageName: "@hot-updater/vercel"
3799
+ },
3800
+ azure: {
3801
+ devDependencies: [],
3802
+ label: "Azure Blob Storage + Functions + Cosmos DB",
3803
+ load: () => import("@hot-updater/azure/init"),
3804
+ packageName: "@hot-updater/azure"
3793
3805
  }
3794
3806
  };
3795
3807
  const isInitProvider = (value) => value !== void 0 && Object.hasOwn(INIT_PROVIDER_PACKAGES, value);
@@ -3870,26 +3882,112 @@ async function handleAgentInfra(operation, options) {
3870
3882
  await handleInfraScaffold(operation, options);
3871
3883
  }
3872
3884
  //#endregion
3873
- //#region src/utils/version/getAndroidVersion.ts
3874
- const getAndroidVersionFromAppBuildGradle = async () => {
3875
- const buildGradlePath = path.join(getCwd(), "android", "app", "build.gradle");
3876
- try {
3877
- return (await fs$3.readFile(buildGradlePath, "utf8")).match(/versionName\s+['"]([^"]+)['"]/)?.[1] ?? null;
3878
- } catch {
3879
- return null;
3880
- }
3885
+ //#region src/utils/version/appVersionCandidates.ts
3886
+ /** One version only when every candidate declares the same literal. */
3887
+ const resolveFromCandidates = (candidates) => {
3888
+ const versions = new Set(candidates.map((candidate) => candidate.version));
3889
+ const [version] = versions;
3890
+ return {
3891
+ version: versions.size === 1 && version ? version : null,
3892
+ candidates
3893
+ };
3881
3894
  };
3882
- const AndroidVersionParsers = { "app-build-gradle": getAndroidVersionFromAppBuildGradle };
3883
- const getAndroidVersion = async ({ parser, validateWithSemver = false }) => {
3884
- const parsers = Array.isArray(parser) ? parser : [parser];
3885
- for (const parserKey of parsers) {
3886
- const parsedVersion = await AndroidVersionParsers[parserKey]();
3887
- if (!parsedVersion) continue;
3888
- if (validateWithSemver && !normalizeRange(parsedVersion)) continue;
3889
- return parsedVersion;
3895
+ /** Several candidates and no single version: the caller must pick one. */
3896
+ const isAmbiguousAppVersion = ({ version, candidates }) => version === null && candidates.length > 1;
3897
+ const formatAppVersionCandidates = (candidates) => candidates.map(({ label, version }) => `${label}: ${version ?? "unresolved"}`).join(", ");
3898
+ //#endregion
3899
+ //#region src/utils/version/getAndroidVersion.ts
3900
+ const stripComments = (source) => source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
3901
+ /** The body of the `{ ... }` block that opens at or after `from`. */
3902
+ const readBlock = (source, from) => {
3903
+ const open = source.indexOf("{", from);
3904
+ if (open === -1) return null;
3905
+ let depth = 0;
3906
+ for (let index = open; index < source.length; index++) {
3907
+ if (source[index] === "{") depth++;
3908
+ if (source[index] === "}" && --depth === 0) return {
3909
+ body: source.slice(open + 1, index),
3910
+ end: index + 1
3911
+ };
3890
3912
  }
3891
3913
  return null;
3892
3914
  };
3915
+ /** The top-level `name { ... }` blocks in a block body. */
3916
+ const readNamedBlocks = (body) => {
3917
+ const blocks = [];
3918
+ const header = /(?:create\(\s*["']([\w-]+)["']\s*\)|([\w-]+))\s*\{/g;
3919
+ let match;
3920
+ while (match = header.exec(body)) {
3921
+ const block = readBlock(body, match.index);
3922
+ if (!block) break;
3923
+ blocks.push({
3924
+ name: match[1] ?? match[2],
3925
+ body: block.body
3926
+ });
3927
+ header.lastIndex = block.end;
3928
+ }
3929
+ return blocks;
3930
+ };
3931
+ /** The first block called `name`, in file order, at any depth. */
3932
+ const findBlock = (source, name) => readNamedBlocks(source).flatMap((block) => block.name === name ? [block] : [findBlock(block.body, name)]).find((block) => block !== void 0);
3933
+ /** A literal setting declared directly in a block body; null when not literal. */
3934
+ const readSetting = (body, setting) => {
3935
+ const match = readNamedBlocks(body).reduce((rest, block) => rest.replace(block.body, ""), body).match(new RegExp(`\\b${setting}\\b\\s*=?\\s*(?:(['"])([^'"]*)\\1|(\\S+))`));
3936
+ if (!match) return void 0;
3937
+ return match[2] || null;
3938
+ };
3939
+ const capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
3940
+ /**
3941
+ * Whether a variant builds a flavor: `stagingRelease` builds `staging`, and
3942
+ * with flavor dimensions `brandAStagingRelease` builds `brandA` and `staging`.
3943
+ */
3944
+ const variantBuildsFlavor = (variant, flavor) => new RegExp(`${capitalize(flavor)}(?=[A-Z]|$)`).test(capitalize(variant));
3945
+ /**
3946
+ * Reads versionName from defaultConfig and from each product flavor, so an
3947
+ * app whose flavors (brands, environments) override the version is reported
3948
+ * as ambiguous instead of resolving to the first versionName in the file.
3949
+ */
3950
+ const getAndroidVersionFromAppBuildGradle = async (scope = {}) => {
3951
+ const buildGradlePath = path.join(getCwd(), "android", scope.appModuleName ?? "app", "build.gradle");
3952
+ let buildGradle;
3953
+ try {
3954
+ buildGradle = stripComments(await fs$3.readFile(buildGradlePath, "utf8"));
3955
+ } catch {
3956
+ return {
3957
+ version: null,
3958
+ candidates: []
3959
+ };
3960
+ }
3961
+ const android = findBlock(buildGradle, "android")?.body ?? "";
3962
+ const defaultConfig = findBlock(android, "defaultConfig");
3963
+ const defaultVersion = defaultConfig ? readSetting(defaultConfig.body, "versionName") : void 0;
3964
+ const flavorsBlock = findBlock(android, "productFlavors");
3965
+ const flavors = (flavorsBlock ? readNamedBlocks(flavorsBlock.body) : []).map((flavor) => ({
3966
+ name: flavor.name,
3967
+ applicationId: readSetting(flavor.body, "applicationId"),
3968
+ versionName: readSetting(flavor.body, "versionName")
3969
+ }));
3970
+ const defaultCandidate = defaultVersion === void 0 ? [] : [{
3971
+ label: "defaultConfig",
3972
+ version: defaultVersion
3973
+ }];
3974
+ const flavorCandidates = (flavorList) => flavorList.flatMap((flavor) => flavor.versionName === void 0 ? [] : [{
3975
+ label: `${flavor.name} flavor`,
3976
+ version: flavor.versionName
3977
+ }]);
3978
+ const { variant, applicationId } = scope;
3979
+ if (variant || applicationId) {
3980
+ const builtFlavors = flavors.filter((flavor) => applicationId !== void 0 && flavor.applicationId === applicationId || variant !== void 0 && variantBuildsFlavor(variant, flavor.name));
3981
+ if (builtFlavors.length > 0) {
3982
+ const overrides = flavorCandidates(builtFlavors);
3983
+ return resolveFromCandidates(overrides.length > 0 ? overrides : defaultCandidate);
3984
+ }
3985
+ }
3986
+ const overrides = flavorCandidates(flavors);
3987
+ const everyFlavorOverrides = flavors.length > 0 && overrides.length === flavors.length;
3988
+ return resolveFromCandidates([...everyFlavorOverrides ? [] : defaultCandidate, ...overrides]);
3989
+ };
3990
+ const resolveAndroidAppVersion = (scope) => getAndroidVersionFromAppBuildGradle(scope);
3893
3991
  //#endregion
3894
3992
  //#region ../../node_modules/.pnpm/@bacons+xcode@1.0.0-alpha.33/node_modules/@bacons/xcode/build/api/utils/constants.js
3895
3993
  var require_constants = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -19406,81 +19504,138 @@ const isFileExist = async (path) => {
19406
19504
  return false;
19407
19505
  }
19408
19506
  };
19409
- const getIOSVersionFromInfoPlist = async () => {
19507
+ const isResolved = (value) => typeof value === "string" && value.length > 0 && !value.includes("$(");
19508
+ const readInfoPlistVersion = async (plistPath) => {
19410
19509
  try {
19411
- const iosAppTargetDirectory = getIosAppTargetDirectoryName();
19412
- if (!iosAppTargetDirectory) return null;
19413
- const plistPath = path.join(getCwd(), "ios", iosAppTargetDirectory, "Info.plist");
19414
19510
  if (!await isFileExist(plistPath)) return null;
19415
19511
  const fileBuffer = await fs$3.readFile(plistPath);
19416
19512
  const appVersion = parsePlist(fileBuffer)["CFBundleShortVersionString"];
19417
- if (typeof appVersion !== "string" || appVersion.includes("$(")) return null;
19418
- return appVersion;
19513
+ return isResolved(appVersion) ? appVersion : null;
19419
19514
  } catch {
19420
19515
  return null;
19421
19516
  }
19422
19517
  };
19423
- const getIOSVersionFromXcodeProject = async () => {
19518
+ const getIOSVersionFromInfoPlist = async () => {
19519
+ const iosAppTargetDirectory = getIosAppTargetDirectoryName();
19520
+ if (!iosAppTargetDirectory) return null;
19521
+ return readInfoPlistVersion(path.join(getCwd(), "ios", iosAppTargetDirectory, "Info.plist"));
19522
+ };
19523
+ const APPLICATION_PRODUCT_TYPE = "com.apple.product-type.application";
19524
+ /**
19525
+ * Reads MARKETING_VERSION per application target, so a project whose targets
19526
+ * (brands, environments) ship different versions is reported as ambiguous
19527
+ * instead of resolving to whichever configuration comes first in the file.
19528
+ */
19529
+ const getIOSVersionFromXcodeProject = async (scope = {}) => {
19530
+ const notFound = {
19531
+ version: null,
19532
+ candidates: []
19533
+ };
19424
19534
  try {
19535
+ const iosDirectory = path.join(getCwd(), "ios");
19425
19536
  const [xcodeprojPath] = import_out.default.globSync("*.xcodeproj/project.pbxproj", {
19426
- cwd: path.join(getCwd(), "ios"),
19537
+ cwd: iosDirectory,
19427
19538
  absolute: true,
19428
19539
  onlyFiles: true
19429
19540
  });
19430
- if (!xcodeprojPath) return null;
19431
- const objects = import_build.XcodeProject.open(xcodeprojPath).toJSON().objects ?? {};
19432
- for (const key of Object.keys(objects)) {
19433
- const obj = objects[key];
19434
- if (!obj || typeof obj !== "object") continue;
19435
- const record = obj;
19436
- if (record.isa !== "XCBuildConfiguration" || record.name !== "Release") continue;
19437
- const marketingVersion = record.buildSettings?.MARKETING_VERSION;
19438
- if (marketingVersion) return marketingVersion;
19439
- }
19440
- return null;
19541
+ if (!xcodeprojPath) return notFound;
19542
+ const project = import_build.XcodeProject.open(xcodeprojPath).toJSON();
19543
+ const objects = project.objects ?? {};
19544
+ const configurationName = scope.configuration ?? "Release";
19545
+ const findConfiguration = (listId) => (listId ? objects[listId]?.buildConfigurations ?? [] : []).map((id) => objects[id]).find((config) => config?.name === configurationName);
19546
+ const rootProject = project.rootObject ? objects[project.rootObject] : void 0;
19547
+ const projectSettings = findConfiguration(rootProject?.buildConfigurationList)?.buildSettings ?? {};
19548
+ const appTargets = (rootProject?.targets ?? []).map((id) => objects[id]).filter((target) => target?.isa === "PBXNativeTarget" && target.productType === APPLICATION_PRODUCT_TYPE).map((target) => {
19549
+ const settings = {
19550
+ ...projectSettings,
19551
+ ...findConfiguration(target.buildConfigurationList)?.buildSettings
19552
+ };
19553
+ const infoPlistFile = typeof settings.INFOPLIST_FILE === "string" ? settings.INFOPLIST_FILE.replace("$(SRCROOT)", iosDirectory) : void 0;
19554
+ return {
19555
+ name: target.name ?? "unknown",
19556
+ bundleIdentifier: settings.PRODUCT_BUNDLE_IDENTIFIER,
19557
+ marketingVersion: settings.MARKETING_VERSION,
19558
+ infoPlistPath: isResolved(infoPlistFile) ? path.resolve(iosDirectory, infoPlistFile) : void 0
19559
+ };
19560
+ });
19561
+ if (appTargets.length === 0) {
19562
+ const candidates = Object.values(objects).filter((object) => object?.isa === "XCBuildConfiguration" && object.name === configurationName && object.buildSettings?.MARKETING_VERSION !== void 0).map((object) => ({
19563
+ label: configurationName,
19564
+ version: isResolved(object?.buildSettings?.MARKETING_VERSION) ? object.buildSettings.MARKETING_VERSION : null
19565
+ }));
19566
+ return resolveFromCandidates(candidates);
19567
+ }
19568
+ const scopedTargets = scope.scheme || scope.bundleIdentifier ? appTargets.filter((target) => target.name === scope.scheme || scope.bundleIdentifier !== void 0 && target.bundleIdentifier === scope.bundleIdentifier) : [];
19569
+ const targets = scopedTargets.length > 0 ? scopedTargets : appTargets;
19570
+ return resolveFromCandidates(await Promise.all(targets.map(async (target) => ({
19571
+ label: `${target.name} (${configurationName})`,
19572
+ version: (target.infoPlistPath ? await readInfoPlistVersion(target.infoPlistPath) : null) ?? (isResolved(target.marketingVersion) ? target.marketingVersion : null)
19573
+ }))));
19441
19574
  } catch {
19442
- return null;
19575
+ return notFound;
19443
19576
  }
19444
19577
  };
19445
- const IOSVersionParsers = {
19446
- xcodeproj: getIOSVersionFromXcodeProject,
19447
- "info-plist": getIOSVersionFromInfoPlist
19448
- };
19449
- const getIOSVersion = async ({ parser, validateWithSemver = false }) => {
19450
- const parsers = Array.isArray(parser) ? parser : [parser];
19451
- for (const parserKey of parsers) {
19452
- const parsedVersion = await IOSVersionParsers[parserKey]();
19453
- if (!parsedVersion) continue;
19454
- if (validateWithSemver && !normalizeRange(parsedVersion)) continue;
19455
- return parsedVersion;
19456
- }
19457
- return null;
19578
+ /**
19579
+ * The iOS app version: Info.plist first, project.pbxproj as a fallback.
19580
+ *
19581
+ * CFBundleShortVersionString is the version the built app actually reports,
19582
+ * so it is the more correct source (#84). It is also far cheaper: reading
19583
+ * Info.plist is a small file read, whereas the pbxproj parser is synchronous
19584
+ * and blocks the event loop for the length of the parse. On a large
19585
+ * project.pbxproj that can take minutes. The React Native template ships
19586
+ * Info.plist with CFBundleShortVersionString set to "$(MARKETING_VERSION)",
19587
+ * which the plist parser cannot resolve, so the real version only lives in
19588
+ * project.pbxproj for most projects.
19589
+ */
19590
+ const resolveIOSAppVersion = async (scope) => {
19591
+ const plistVersion = await getIOSVersionFromInfoPlist();
19592
+ if (plistVersion) return {
19593
+ version: plistVersion,
19594
+ candidates: [{
19595
+ label: "Info.plist",
19596
+ version: plistVersion
19597
+ }]
19598
+ };
19599
+ return getIOSVersionFromXcodeProject(scope);
19458
19600
  };
19459
19601
  //#endregion
19460
19602
  //#region src/utils/version/getNativeAppVersion.ts
19461
- const getNativeAppVersion = async (platform) => {
19603
+ /**
19604
+ * The version the native project builds, with every place it is declared.
19605
+ * A project that builds several apps (Xcode targets, product flavors) with
19606
+ * different versions resolves to null unless `scope` names the one built.
19607
+ */
19608
+ const resolveNativeAppVersion = async (platform, scope = {}) => {
19462
19609
  switch (platform) {
19463
- case "ios": return getIOSVersion({ parser: ["info-plist", "xcodeproj"] });
19464
- case "android": return getAndroidVersion({ parser: "app-build-gradle" });
19465
- default: return null;
19610
+ case "ios": return resolveIOSAppVersion(scope.ios);
19611
+ case "android": return resolveAndroidAppVersion(scope.android);
19612
+ default: return {
19613
+ version: null,
19614
+ candidates: []
19615
+ };
19466
19616
  }
19467
19617
  };
19618
+ const getNativeAppVersion = async (platform, scope) => (await resolveNativeAppVersion(platform, scope)).version;
19468
19619
  //#endregion
19469
19620
  //#region src/commands/appVersion.ts
19470
19621
  const readAppVersions = async () => {
19471
- const [androidVersion, iosVersion] = await Promise.all([getNativeAppVersion("android"), getNativeAppVersion("ios")]);
19622
+ const [android, ios] = await Promise.all([resolveNativeAppVersion("android"), resolveNativeAppVersion("ios")]);
19472
19623
  return {
19473
- android: androidVersion,
19474
- ios: iosVersion
19624
+ android,
19625
+ ios
19475
19626
  };
19476
19627
  };
19628
+ const formatVersion = (resolved) => isAmbiguousAppVersion(resolved) ? ui.warning(`several (${formatAppVersionCandidates(resolved.candidates)}); deploy needs -t`) : ui.version(resolved.version);
19477
19629
  const handleAppVersion = async (options = {}) => {
19478
19630
  const versions = await readAppVersions();
19479
19631
  if (options.json) {
19480
- console.log(JSON.stringify(versions, null, 2));
19632
+ console.log(JSON.stringify({
19633
+ android: versions.android.version,
19634
+ ios: versions.ios.version
19635
+ }, null, 2));
19481
19636
  return;
19482
19637
  }
19483
- p.log.message(ui.block("App version", [ui.kv("Android", ui.version(versions.android)), ui.kv("iOS", ui.version(versions.ios))]));
19638
+ p.log.message(ui.block("App version", [ui.kv("Android", formatVersion(versions.android)), ui.kv("iOS", formatVersion(versions.ios))]));
19484
19639
  };
19485
19640
  //#endregion
19486
19641
  //#region ../../node_modules/.pnpm/is-plain-obj@4.1.0/node_modules/is-plain-obj/index.js
@@ -25767,11 +25922,25 @@ async function prepareNativeBuild(options) {
25767
25922
  } else if (config.updateStrategy === "appVersion") {
25768
25923
  const s = p.spinner();
25769
25924
  s.start(`Get native app version (${platform})`);
25770
- const appVersion = await getNativeAppVersion(platform);
25925
+ const iosScheme = config.nativeBuild.ios[scheme];
25926
+ const androidScheme = config.nativeBuild.android[scheme];
25927
+ const resolved = await resolveNativeAppVersion(platform, {
25928
+ ios: iosScheme && {
25929
+ scheme: iosScheme.scheme,
25930
+ bundleIdentifier: iosScheme.bundleIdentifier,
25931
+ configuration: iosScheme.configuration
25932
+ },
25933
+ android: androidScheme && {
25934
+ variant: androidScheme.variant,
25935
+ applicationId: androidScheme.applicationId ?? androidScheme.packageName,
25936
+ appModuleName: androidScheme.appModuleName
25937
+ }
25938
+ });
25939
+ const appVersion = resolved.version;
25771
25940
  s.stop(`App Version(${platform}): ${appVersion}`);
25772
25941
  target.appVersion = appVersion;
25773
25942
  if (!target.appVersion) {
25774
- p.log.error(`Failed to retrieve native app version of ${platform}`);
25943
+ p.log.error(isAmbiguousAppVersion(resolved) ? platform === "ios" ? `nativeBuild.ios.${scheme} (scheme ${iosScheme?.scheme}, bundleIdentifier ${iosScheme?.bundleIdentifier}) does not pick one of the app targets in project.pbxproj, which declare different versions (${formatAppVersionCandidates(resolved.candidates)}). Make its scheme match the name of the app target it builds.` : `nativeBuild.android.${scheme} (variant ${androidScheme?.variant ?? "Release"}) does not pick one of the product flavors that set versionName, which declare different versions (${formatAppVersionCandidates(resolved.candidates)}). Set its variant to the flavor it builds, such as brandARelease.` : `Failed to retrieve native app version of ${platform}`);
25775
25944
  return null;
25776
25945
  }
25777
25946
  }
@@ -26913,18 +27082,23 @@ async function validateSigningConfig(config, options = {}) {
26913
27082
  }
26914
27083
  //#endregion
26915
27084
  //#region src/utils/version/getDefaultTargetAppVersion.ts
27085
+ /**
27086
+ * The target app version deploy defaults to, as a range. `version` is null
27087
+ * when the native files declare none, or several (see `candidates`).
27088
+ */
26916
27089
  const getDefaultTargetAppVersion = async (platform) => {
26917
- let version = null;
26918
- switch (platform) {
26919
- case "ios":
26920
- version = await getIOSVersion({ parser: ["info-plist", "xcodeproj"] });
26921
- break;
26922
- case "android": version = await getAndroidVersion({ parser: "app-build-gradle" });
26923
- }
26924
- if (!version) return null;
26925
- if (!(/^\d+\.\d+$/.test(version) || normalizeRange(version))) return null;
27090
+ const resolved = await resolveNativeAppVersion(platform);
27091
+ let version = resolved.version;
27092
+ if (!version) return resolved;
27093
+ if (!(/^\d+\.\d+$/.test(version) || normalizeRange(version))) return {
27094
+ ...resolved,
27095
+ version: null
27096
+ };
26926
27097
  if (version.split(".").length - 1 === 1) version = `${version}.x`;
26927
- return version;
27098
+ return {
27099
+ ...resolved,
27100
+ version
27101
+ };
26928
27102
  };
26929
27103
  //#endregion
26930
27104
  //#region src/commands/deployTransaction.ts
@@ -27250,18 +27424,20 @@ const deployPlatform = async ({ config, core, database, deferAutoPatches, option
27250
27424
  target.fingerprintHash = newFingerprint.hash;
27251
27425
  s.stop(`Fingerprint(${platform}): ${newFingerprint.hash}`);
27252
27426
  } else {
27253
- const defaultTargetAppVersion = await getDefaultTargetAppVersion(platform);
27427
+ const defaultTargetAppVersion = options.targetAppVersion ? null : await getDefaultTargetAppVersion(platform);
27428
+ const ambiguousVersions = defaultTargetAppVersion && isAmbiguousAppVersion(defaultTargetAppVersion) ? formatAppVersionCandidates(defaultTargetAppVersion.candidates) : null;
27429
+ const defaultVersion = defaultTargetAppVersion?.version ?? null;
27254
27430
  const targetAppVersion = options.targetAppVersion ?? (options.interactive ? await p.text({
27255
- message: "Target app version",
27256
- placeholder: defaultTargetAppVersion ?? "1.0.0",
27257
- initialValue: defaultTargetAppVersion ?? "1.0.0",
27431
+ message: ambiguousVersions ? `Target app version (the native project builds several versions: ${ambiguousVersions})` : "Target app version",
27432
+ placeholder: defaultVersion ?? "1.0.0",
27433
+ initialValue: defaultVersion ?? (ambiguousVersions ? "" : "1.0.0"),
27258
27434
  validate: (value) => {
27259
27435
  if (!value || !normalizeRange(value)) return "Invalid semver format (e.g. 1.0.0, 1.x.x)";
27260
27436
  }
27261
- }) : defaultTargetAppVersion);
27437
+ }) : defaultVersion);
27262
27438
  if (p.isCancel(targetAppVersion)) return null;
27263
27439
  if (!targetAppVersion) {
27264
- p.log.error("Target app version not found in native files (Info.plist for iOS, build.gradle for Android). Pass -t <targetAppVersion> explicitly, or check your native config.");
27440
+ p.log.error(ambiguousVersions ? `The native project builds several app versions (${ambiguousVersions}), so the target app version for this deploy cannot be read from it. Pass -t <targetAppVersion> for the app this deploy targets.` : "Target app version not found in native files (Info.plist or project.pbxproj for iOS, build.gradle for Android). Pass -t <targetAppVersion> explicitly, or check your native config.");
27265
27441
  return null;
27266
27442
  }
27267
27443
  target.appVersion = targetAppVersion;
@@ -29639,7 +29815,7 @@ const isObject$2 = (value) => value !== null && typeof value === "object" && !Ar
29639
29815
  const isText$1 = (value) => typeof value === "string" && value.trim().length > 0;
29640
29816
  //#endregion
29641
29817
  //#region src/commands/doctor/scaffold.ts
29642
- const isDeploymentInput = (file) => /^(worker|supabase|lambda|cloudfront|dynamodb|iam|firebase)\//.test(file) && !/\.(md|map)$/.test(file);
29818
+ const isDeploymentInput = (file) => /^(worker|supabase|lambda|cloudfront|dynamodb|iam|firebase|vercel|azure)\//.test(file) && !/\.(md|map)$/.test(file);
29643
29819
  const hasPlaceholder = (text) => /__HOT_UPDATER_[A-Z0-9_]+__|%%BUCKET_NAME%%/.test(text);
29644
29820
  async function readScaffoldFile(root, file) {
29645
29821
  const resolved = path$1.resolve(root, file);
@@ -29783,6 +29959,19 @@ async function checkScaffold(infraDir) {
29783
29959
  const functions = json("firebase/firebase.json")["functions"];
29784
29960
  if (!(Array.isArray(functions) ? functions : [functions]).some((entry) => isObject$2(entry) && entry["source"] === "functions")) fail("INFRA_FIREBASE_FUNCTION", "Firebase must deploy the supplied functions directory.", "firebase/firebase.json", "Restore the functions source from the supplied Firebase configuration.");
29785
29961
  }
29962
+ if (manifest.provider === "vercel") {
29963
+ const file = "vercel/vercel.json";
29964
+ if (!items(json(file)["rewrites"]).some((entry) => entry["source"] === "/(.*)" && entry["destination"] === "/api")) fail("INFRA_VERCEL_ROUTES", "Vercel must rewrite every path to the supplied Function.", file, "Restore the rewrite { \"source\": \"/(.*)\", \"destination\": \"/api\" }.");
29965
+ try {
29966
+ await readScaffoldFile(infraDir, "vercel/api/index.mjs");
29967
+ } catch {
29968
+ fail("INFRA_RUNTIME_ENTRY", "The supplied Vercel Function is missing or unsafe.", "vercel/api/index.mjs", "Restore api/index.mjs from the supplied Vercel scaffold.");
29969
+ }
29970
+ }
29971
+ if (manifest.provider === "azure") {
29972
+ if (object(object(json("azure/function/host.json")["extensions"])["http"])["routePrefix"] !== "") fail("INFRA_AZURE_ROUTES", "Azure Functions must serve the supplied Function without a route prefix.", "azure/function/host.json", "Restore extensions.http.routePrefix to \"\" in host.json.");
29973
+ if (json("azure/function/package.json")["main"] !== "index.mjs") fail("INFRA_RUNTIME_ENTRY", "The Azure Function package must load the supplied index.mjs.", "azure/function/package.json", "Restore \"main\": \"index.mjs\" from the supplied Azure scaffold.");
29974
+ }
29786
29975
  if (checks.every((check) => check.status === "pass")) checks.push({
29787
29976
  code: "INFRA_SCAFFOLD",
29788
29977
  status: "pass",
@@ -222,7 +222,9 @@ separate validation result.
222
222
  bucket using the exact local storage plugin's credential chain and endpoint:
223
223
  Cloudflare R2 S3 / AWS S3 `ListObjectsV2` with MaxKeys=1; Supabase Storage list
224
224
  with limit=1; Firebase Admin Storage getFiles with maxResults=1 and
225
- autoPaginate=false. Load credentials privately in the probe process and report
225
+ autoPaginate=false; Vercel Blob `list({ limit: 1 })` with the local read-write
226
+ token; Azure Blob Storage `listBlobsFlat` with maxPageSize=1. Load
227
+ credentials privately in the probe process and report
226
228
  only success/count, never keys or object contents. A provider MCP session or
227
229
  the deployed runtime's access cannot substitute for local plugin access.
228
230
  - Verify/record: config loads with the intended provider/build, the server check
@@ -21,7 +21,8 @@ if (
21
21
  const { api, clientAuth, database, plugins } = hotUpdater;
22
22
 
23
23
  const credentialPath = new URL("./client-credential.local", import.meta.url);
24
- // Providers without migration tooling (Firestore) ship migrate.ts.
24
+ // Providers whose schema comes from the server's migrator (Firestore, Cosmos
25
+ // DB, and Vercel's PostgreSQL) ship migrate.ts.
25
26
  const migrationPath = new URL("./migrate.ts", import.meta.url);
26
27
  try {
27
28
  // The plugin that protects the deployed server's client routes, if any,
@@ -64,8 +65,9 @@ try {
64
65
  }
65
66
  }
66
67
  if (existsSync(migrationPath)) {
67
- // It writes the schema settings of core and the deployed server's
68
- // plugins, which the database checks before its first read.
68
+ // It creates the tables of core and the deployed server's plugins where
69
+ // the database keeps tables, and writes their schema settings, which the
70
+ // database checks before its first read.
69
71
  const { migrate } = await import(migrationPath.href);
70
72
  await migrate(hotUpdater);
71
73
  }
@@ -4550,7 +4550,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
4550
4550
  };
4551
4551
  //#endregion
4552
4552
  //#region ../../packages/server/dist/version.mjs
4553
- const HOT_UPDATER_SERVER_VERSION = "1.0.0";
4553
+ const HOT_UPDATER_SERVER_VERSION = "1.0.2";
4554
4554
  //#endregion
4555
4555
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4556
4556
  /**
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "provider": "aws",
4
- "cliVersion": "1.0.0",
5
- "providerVersion": "1.0.0",
6
- "serverVersion": "1.0.0",
4
+ "cliVersion": "1.0.2",
5
+ "providerVersion": "1.0.2",
6
+ "serverVersion": "1.0.2",
7
7
  "infrastructureGeneration": 1,
8
8
  "clientAuth": {
9
9
  "plugin": "apiKeys",
@@ -65,13 +65,13 @@
65
65
  }
66
66
  ],
67
67
  "packages": {
68
- "hot-updater": "1.0.0",
69
- "@hot-updater/server": "1.0.0",
70
- "@hot-updater/react-native": "1.0.0",
71
- "@hot-updater/aws": "1.0.0",
72
- "@hot-updater/bare": "1.0.0",
73
- "@hot-updater/rock": "1.0.0",
74
- "@hot-updater/expo": "1.0.0",
68
+ "hot-updater": "1.0.2",
69
+ "@hot-updater/server": "1.0.2",
70
+ "@hot-updater/react-native": "1.0.2",
71
+ "@hot-updater/aws": "1.0.2",
72
+ "@hot-updater/bare": "1.0.2",
73
+ "@hot-updater/rock": "1.0.2",
74
+ "@hot-updater/expo": "1.0.2",
75
75
  "@aws-sdk/credential-providers": "3.1066.0"
76
76
  },
77
77
  "requiredInputs": {
@@ -4,6 +4,10 @@ Inspect the live server version, infrastructure generation and migration history
4
4
 
5
5
  Each file records one required infrastructure version. Read the applicable baseline for the installed generation as context, then every subsequent file through the target requirement in ascending order. If the deployed generation predates the first file, start with that file. Read all relevant files before applying any changes; do not skip intermediate releases or read only the newest file.
6
6
 
7
+ Vercel was added after 1.0.0: files through 1.0.0 have no Vercel section. A Vercel deployment applies the Vercel sections of the files after 1.0.0; when none is listed, it needs no infrastructure change, and reconciling it with this scaffold follows the upgrade section of its SETUP.md.
8
+
9
+ Azure was added after 1.0.0: files through 1.0.0 have no Azure section. A Azure deployment applies the Azure sections of the files after 1.0.0; when none is listed, it needs no infrastructure change, and reconciling it with this scaffold follows the upgrade section of its SETUP.md.
10
+
7
11
  Use the common sections and your provider's section to plan the full transition. Previously completed steps provide context and must not be replayed blindly. Inspect actual migration/resource state, preserve customizations and secrets, and record which version files and steps have been applied and verified. Apply pending changes in order using the target scaffold; investigate conflicting requirements before proceeding.
8
12
 
9
13
  - [1.0.0](./1.0.0.md) — Release Catalog, manifest artifact protocol v1, and storage engine infrastructure generation