81 lines
5.2 KiB
TypeScript
81 lines
5.2 KiB
TypeScript
// Store build minimums, one dated table per store: a new year is one new row.
|
|
// - Google Play: the target API level (targetSdkVersion) that new apps and app updates need. Since 2026-08-31 Google's
|
|
// rule is the same for new apps and updates (an extension to 2026-11-01 can be requested in Play Console).
|
|
// https://developer.android.com/google/play/requirements/target-sdk
|
|
// - App Store: the Xcode / SDK an upload must be built with (Apple's "Upcoming requirements").
|
|
// https://developer.apple.com/news/upcoming-requirements/
|
|
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
export type FormFactor = 'phone' | 'wear' | 'tv' | 'automotive';
|
|
|
|
/** Google Play target API level per form factor, by the date it is enforced (oldest first). */
|
|
export const PLAY_TARGET_SDK_TABLE: readonly { since: string; extensionUntil?: string; min: Record<FormFactor, number> }[] = [
|
|
{ since: '2025-08-31', extensionUntil: '2025-11-01', min: { phone: 35, wear: 34, automotive: 34, tv: 34 } },
|
|
{ since: '2026-08-31', extensionUntil: '2026-11-01', min: { phone: 36, wear: 35, automotive: 35, tv: 34 } },
|
|
];
|
|
|
|
/** App Store: uploads must be built with at least this Xcode and platform SDK (iOS / iPadOS etc.), by date. */
|
|
export const APPLE_MIN_SDK_TABLE: readonly { since: string; xcode: number; sdk: number }[] = [
|
|
{ since: '2025-04-24', xcode: 16, sdk: 18 },
|
|
{ since: '2026-04-28', xcode: 26, sdk: 26 },
|
|
];
|
|
|
|
const today = (): string => new Date().toISOString().slice(0, 10);
|
|
function rowAt<T extends { since: string }>(t: readonly T[], date: string): T | null {
|
|
let r: T | null = null;
|
|
for (const x of t) if (x.since <= date) r = x;
|
|
return r;
|
|
}
|
|
|
|
/** The Play target API level in force on `date` (default today). */
|
|
export function playTargetSdk(form: FormFactor = 'phone', date = today()): { min: number; since: string; extensionUntil?: string } | null {
|
|
const r = rowAt(PLAY_TARGET_SDK_TABLE, date);
|
|
return r ? { min: r.min[form], since: r.since, extensionUntil: r.extensionUntil } : null;
|
|
}
|
|
|
|
/** Why Play refuses this targetSdk, with the fix; null when it is fine (or unknown). */
|
|
export function playTargetSdkProblem(target: number | undefined | null, o: { form?: FormFactor; date?: string; where?: string } = {}): string | null {
|
|
const r = playTargetSdk(o.form ?? 'phone', o.date);
|
|
if (!r || !target || target >= r.min) return null;
|
|
const where = o.where ?? 'android/variables.gradle (or app/build.gradle)';
|
|
return `targetSdkVersion ${target} < ${r.min}: Google Play takes new apps and updates only for API ${r.min}+ since ${r.since}` +
|
|
`${o.form && o.form !== 'phone' ? ` (${o.form})` : ''}; set targetSdkVersion = ${r.min} (and compileSdkVersion) in ${where} and rebuild` +
|
|
(r.extensionUntil ? ` (an extension to ${r.extensionUntil} can be requested in Play Console)` : '');
|
|
}
|
|
|
|
/** The App Store minimum in force on `date` (default today). */
|
|
export function appleMinSdk(date = today()): { xcode: number; sdk: number; since: string } | null {
|
|
return rowAt(APPLE_MIN_SDK_TABLE, date);
|
|
}
|
|
|
|
/** Xcode major from DTXcode ("2600" -> 26, "1640" -> 16). */
|
|
export const xcodeMajor = (dt: string | undefined): number | null => (dt && /^\d{3,4}$/.test(dt) ? Math.floor(Number(dt) / 100) : null);
|
|
/** SDK major from DTPlatformVersion ("26.0") or DTSDKName ("iphoneos26.0"). */
|
|
export function sdkMajor(platformVersion: string | undefined, sdkName?: string): number | null {
|
|
const m = (platformVersion ?? '').match(/^(\d+)/) ?? (sdkName ?? '').match(/(\d+)(?:\.\d+)*$/);
|
|
return m ? Number(m[1]) : null;
|
|
}
|
|
|
|
/** Why App Store Connect refuses an IPA built with this Xcode / SDK, with the fix; null when fine (or unknown). */
|
|
export function appleSdkProblem(b: { xcode?: string; sdkName?: string; platformVersion?: string }, date?: string): string | null {
|
|
const r = appleMinSdk(date);
|
|
if (!r) return null;
|
|
const x = xcodeMajor(b.xcode), s = sdkMajor(b.platformVersion, b.sdkName);
|
|
if ((x === null || x >= r.xcode) && (s === null || s >= r.sdk)) return null;
|
|
const got = [x !== null ? `Xcode ${x}` : '', b.sdkName ?? (s !== null ? `SDK ${s}` : '')].filter(Boolean).join(', ');
|
|
return `built with ${got} < Xcode ${r.xcode} / SDK ${r.sdk}: App Store Connect takes uploads only from Xcode ${r.xcode}+ with the ${r.sdk} SDKs since ${r.since}; ` +
|
|
`install Xcode ${r.xcode} (xcode-select -s), rebuild (storebound apple build) and upload the new IPA`;
|
|
}
|
|
|
|
/** The project file that sets targetSdkVersion (Capacitor variables.gradle, Gradle app module, Unity ProjectSettings),
|
|
* for the fix line; a generic hint when none is found. */
|
|
export function targetSdkFile(root: string): string {
|
|
const files = ['android/variables.gradle', 'android/app/build.gradle', 'android/app/build.gradle.kts', 'app/build.gradle', 'app/build.gradle.kts',
|
|
'android/build.gradle', 'android/build.gradle.kts', 'ProjectSettings/ProjectSettings.asset'];
|
|
for (const f of files) {
|
|
try { if (existsSync(join(root, f)) && /targetSdk|AndroidTargetSdkVersion/.test(readFileSync(join(root, f), 'utf8'))) return f.endsWith('.asset') ? `${f} (AndroidTargetSdkVersion; Player Settings > Target API Level)` : f; } catch { /* unreadable */ }
|
|
}
|
|
return 'the app module\'s build.gradle (Capacitor: android/variables.gradle; Flutter: android/app/build.gradle; Unity: Player Settings > Target API Level)';
|
|
}
|