1import assert from 'assert'; 2import chalk from 'chalk'; 3 4import { env } from './env'; 5import { memoize } from './fn'; 6import { learnMore } from './link'; 7import { isUrlAvailableAsync } from './url'; 8import { fetchAsync } from '../api/rest/client'; 9import { Log } from '../log'; 10 11const debug = require('debug')('expo:utils:validateApplicationId') as typeof console.log; 12 13const IOS_BUNDLE_ID_REGEX = /^[a-zA-Z0-9-.]+$/; 14const ANDROID_PACKAGE_REGEX = /^(?!.*\bnative\b)[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/; 15 16/** Validate an iOS bundle identifier. */ 17export function validateBundleId(value: string): boolean { 18 return IOS_BUNDLE_ID_REGEX.test(value); 19} 20 21/** Validate an Android package name. */ 22export function validatePackage(value: string): boolean { 23 return validatePackageWithWarning(value) === true; 24} 25 26/** Validate an Android package name and return the reason if invalid. */ 27export function validatePackageWithWarning(value: string): true | string { 28 const parts = value.split('.'); 29 for (const segment of parts) { 30 if (RESERVED_ANDROID_PACKAGE_NAME_SEGMENTS.includes(segment)) { 31 return `"${segment}" is a reserved Java keyword.`; 32 } 33 } 34 if (parts.length < 2) { 35 return `Package name must contain more than one segment, separated by ".", e.g. com.${value}`; 36 } 37 if (!ANDROID_PACKAGE_REGEX.test(value)) { 38 return 'Invalid characters in Android package name. Only alphanumeric characters, "." and "_" are allowed, and each "." must be followed by a letter or number.'; 39 } 40 41 return true; 42} 43 44// https://en.wikipedia.org/wiki/List_of_Java_keywords 45// Running the following in the console and pruning the "Reserved Identifiers" section: 46// [...document.querySelectorAll('dl > dt > code')].map(node => node.innerText) 47const RESERVED_ANDROID_PACKAGE_NAME_SEGMENTS = [ 48 // List of Java keywords 49 '_', 50 'abstract', 51 'assert', 52 'boolean', 53 'break', 54 'byte', 55 'case', 56 'catch', 57 'char', 58 'class', 59 'const', 60 'continue', 61 'default', 62 'do', 63 'double', 64 'else', 65 'enum', 66 'extends', 67 'final', 68 'finally', 69 'float', 70 'for', 71 'goto', 72 'if', 73 'implements', 74 'import', 75 'instanceof', 76 'int', 77 'interface', 78 'long', 79 'native', 80 'new', 81 'package', 82 'private', 83 'protected', 84 'public', 85 'return', 86 'short', 87 'static', 88 'super', 89 'switch', 90 'synchronized', 91 'this', 92 'throw', 93 'throws', 94 'transient', 95 'try', 96 'void', 97 'volatile', 98 'while', 99 // Reserved words for literal values 100 'true', 101 'false', 102 'null', 103 // Unused 104 'const', 105 'goto', 106 'strictfp', 107]; 108 109export function assertValidBundleId(value: string) { 110 assert.match( 111 value, 112 IOS_BUNDLE_ID_REGEX, 113 `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.` 114 ); 115} 116 117export function assertValidPackage(value: string) { 118 assert.match( 119 value, 120 ANDROID_PACKAGE_REGEX, 121 `Invalid format of Android package name. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter. The Java keyword 'native' is not allowed.` 122 ); 123} 124 125/** @private */ 126export async function getBundleIdWarningInternalAsync(bundleId: string): Promise<string | null> { 127 if (env.EXPO_OFFLINE) { 128 Log.warn('Skipping Apple bundle identifier reservation validation in offline-mode.'); 129 return null; 130 } 131 132 if (!(await isUrlAvailableAsync('itunes.apple.com'))) { 133 debug( 134 `Couldn't connect to iTunes Store to check bundle ID ${bundleId}. itunes.apple.com may be down.` 135 ); 136 // If no network, simply skip the warnings since they'll just lead to more confusion. 137 return null; 138 } 139 140 const url = `http://itunes.apple.com/lookup?bundleId=${bundleId}`; 141 try { 142 debug(`Checking iOS bundle ID '${bundleId}' at: ${url}`); 143 const response = await fetchAsync(url); 144 const json = await response.json(); 145 if (json.resultCount > 0) { 146 const firstApp = json.results[0]; 147 return formatInUseWarning(firstApp.trackName, firstApp.sellerName, bundleId); 148 } 149 } catch (error: any) { 150 debug(`Error checking bundle ID ${bundleId}: ${error.message}`); 151 // Error fetching itunes data. 152 } 153 return null; 154} 155 156/** Returns a warning message if an iOS bundle identifier is potentially already in use. */ 157export const getBundleIdWarningAsync = memoize(getBundleIdWarningInternalAsync); 158 159/** @private */ 160export async function getPackageNameWarningInternalAsync( 161 packageName: string 162): Promise<string | null> { 163 if (env.EXPO_OFFLINE) { 164 Log.warn('Skipping Android package name reservation validation in offline-mode.'); 165 return null; 166 } 167 168 if (!(await isUrlAvailableAsync('play.google.com'))) { 169 debug( 170 `Couldn't connect to Play Store to check package name ${packageName}. play.google.com may be down.` 171 ); 172 // If no network, simply skip the warnings since they'll just lead to more confusion. 173 return null; 174 } 175 176 const url = `https://play.google.com/store/apps/details?id=${packageName}`; 177 try { 178 debug(`Checking Android package name '${packageName}' at: ${url}`); 179 const response = await fetchAsync(url); 180 // If the page exists, then warn the user. 181 if (response.status === 200) { 182 // There is no JSON API for the Play Store so we can't concisely 183 // locate the app name and developer to match the iOS warning. 184 return `⚠️ The package ${chalk.bold(packageName)} is already in use. ${chalk.dim( 185 learnMore(url) 186 )}`; 187 } 188 } catch (error: any) { 189 // Error fetching play store data or the page doesn't exist. 190 debug(`Error checking package name ${packageName}: ${error.message}`); 191 } 192 return null; 193} 194 195function formatInUseWarning(appName: string, author: string, id: string): string { 196 return `⚠️ The app ${chalk.bold(appName)} by ${chalk.italic( 197 author 198 )} is already using ${chalk.bold(id)}`; 199} 200 201/** Returns a warning message if an Android package name is potentially already in use. */ 202export const getPackageNameWarningAsync = memoize(getPackageNameWarningInternalAsync); 203