1import assert from 'assert'; 2import chalk from 'chalk'; 3import fetch from 'node-fetch'; 4 5import { learnMore } from './link'; 6import { isUrlAvailableAsync } from './url'; 7 8const IOS_BUNDLE_ID_REGEX = /^[a-zA-Z0-9-.]+$/; 9const ANDROID_PACKAGE_REGEX = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/; 10 11/** Validate an iOS bundle identifier. */ 12export function validateBundleId(value: string): boolean { 13 return IOS_BUNDLE_ID_REGEX.test(value); 14} 15 16/** Validate an Android package name. */ 17export function validatePackage(value: string): boolean { 18 return ANDROID_PACKAGE_REGEX.test(value); 19} 20 21export function assertValidBundleId(value: string) { 22 assert.match( 23 value, 24 IOS_BUNDLE_ID_REGEX, 25 `The ios.bundleIdentifier defined in your Expo config is not formatted properly. Only alphanumeric characters, '.', '-', and '_' are allowed, and each '.' must be followed by a letter.` 26 ); 27} 28 29export function assertValidPackage(value: string) { 30 assert.match( 31 value, 32 ANDROID_PACKAGE_REGEX, 33 `Invalid format of Android package name. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.` 34 ); 35} 36 37const cachedBundleIdResults: Record<string, string> = {}; 38const cachedPackageNameResults: Record<string, string> = {}; 39 40/** Returns a warning message if an iOS bundle identifier is potentially already in use. */ 41export async function getBundleIdWarningAsync(bundleId: string): Promise<string | null> { 42 // Prevent fetching for the same ID multiple times. 43 if (cachedBundleIdResults[bundleId]) { 44 return cachedBundleIdResults[bundleId]; 45 } 46 47 if (!(await isUrlAvailableAsync('itunes.apple.com'))) { 48 // If no network, simply skip the warnings since they'll just lead to more confusion. 49 return null; 50 } 51 52 const url = `http://itunes.apple.com/lookup?bundleId=${bundleId}`; 53 try { 54 const response = await fetch(url); 55 const json = await response.json(); 56 if (json.resultCount > 0) { 57 const firstApp = json.results[0]; 58 const message = formatInUseWarning(firstApp.trackName, firstApp.sellerName, bundleId); 59 cachedBundleIdResults[bundleId] = message; 60 return message; 61 } 62 } catch { 63 // Error fetching itunes data. 64 } 65 return null; 66} 67 68/** Returns a warning message if an Android package name is potentially already in use. */ 69export async function getPackageNameWarningAsync(packageName: string): Promise<string | null> { 70 // Prevent fetching for the same ID multiple times. 71 if (cachedPackageNameResults[packageName]) { 72 return cachedPackageNameResults[packageName]; 73 } 74 75 if (!(await isUrlAvailableAsync('play.google.com'))) { 76 // If no network, simply skip the warnings since they'll just lead to more confusion. 77 return null; 78 } 79 80 const url = `https://play.google.com/store/apps/details?id=${packageName}`; 81 try { 82 const response = await fetch(url); 83 // If the page exists, then warn the user. 84 if (response.status === 200) { 85 // There is no JSON API for the Play Store so we can't concisely 86 // locate the app name and developer to match the iOS warning. 87 const message = `⚠️ The package ${chalk.bold(packageName)} is already in use. ${chalk.dim( 88 learnMore(url) 89 )}`; 90 cachedPackageNameResults[packageName] = message; 91 return message; 92 } 93 } catch { 94 // Error fetching play store data or the page doesn't exist. 95 } 96 return null; 97} 98 99function formatInUseWarning(appName: string, author: string, id: string): string { 100 return `⚠️ The app ${chalk.bold(appName)} by ${chalk.italic( 101 author 102 )} is already using ${chalk.bold(id)}`; 103} 104