runcloud 0.2.153 → 0.2.155

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.
@@ -1,13 +1,13 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync, } from 'node:fs';
2
2
  import path from 'node:path';
3
+ const PREVIEW_APP_ID = 'app.newly.preview';
3
4
  const CONFIG_RULES = {
4
5
  app_name: { path: ['expo', 'name'], absent: 'fail' },
5
6
  slug: { path: ['expo', 'slug'], absent: 'fail' },
6
- ios_bundle_id: { path: ['expo', 'ios', 'bundleIdentifier'], absent: 'fail' },
7
- android_package: { path: ['expo', 'android', 'package'], absent: 'fail' },
7
+ ios_bundle_id: { path: ['expo', 'ios', 'bundleIdentifier'], absent: 'info', storeId: /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$/ },
8
+ android_package: { path: ['expo', 'android', 'package'], absent: 'info', storeId: /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/ },
8
9
  version: { path: ['expo', 'version'], absent: 'warning' },
9
10
  scheme: { path: ['expo', 'scheme'], absent: 'info' },
10
- ios_encryption: { path: ['expo', 'ios', 'infoPlist', 'ITSAppUsesNonExemptEncryption'], absent: 'warning' },
11
11
  user_interface_style: { path: ['expo', 'userInterfaceStyle'], absent: 'info' },
12
12
  orientation: { path: ['expo', 'orientation'], absent: 'info' },
13
13
  ios_supports_tablet: { path: ['expo', 'ios', 'supportsTablet'], absent: 'info' },
@@ -183,6 +183,8 @@ function evaluateLocalItem(root, appJson, item) {
183
183
  return evaluateExpoLayout(root, item);
184
184
  case 'expo_duplicate_route':
185
185
  return evaluateDuplicateRoutes(root, item);
186
+ case 'ios_encryption':
187
+ return evaluateEncryption(appJson, item);
186
188
  case 'plugin_permission_missing':
187
189
  return evaluatePluginPermissions(root, appJson, item);
188
190
  case 'permission_descriptions':
@@ -202,10 +204,25 @@ function evaluateConfigItem(appJson, item, rule) {
202
204
  const value = getPath(appJson, rule.path);
203
205
  const present = hasValue(value);
204
206
  const evidence = `app.json ${rule.path.join('.')} = ${value === undefined ? 'undefined' : JSON.stringify(value)}`;
207
+ if (rule.storeId) {
208
+ const id = stringValue(value);
209
+ if (!id || id.toLowerCase() === PREVIEW_APP_ID)
210
+ return makeItem(item, rule.absent, `${item.label} is assigned when you publish.`, evidence);
211
+ if (!rule.storeId.test(id) || id.length > 255)
212
+ return makeItem(item, 'fail', `${item.label} is not valid. Use a reverse-domain ID like com.yourname.app.`, evidence);
213
+ }
205
214
  if (present)
206
215
  return makeItem(item, 'pass', `${item.label} is set.`, evidence);
207
216
  return makeItem(item, rule.absent, `${item.label} is not set.`, evidence);
208
217
  }
218
+ function evaluateEncryption(appJson, item) {
219
+ const plist = getPath(appJson, ['expo', 'ios', 'infoPlist', 'ITSAppUsesNonExemptEncryption']);
220
+ const config = getPath(appJson, ['expo', 'ios', 'config', 'usesNonExemptEncryption']);
221
+ const evidence = `app.json expo.ios.infoPlist.ITSAppUsesNonExemptEncryption = ${JSON.stringify(plist) ?? 'undefined'}, expo.ios.config.usesNonExemptEncryption = ${JSON.stringify(config) ?? 'undefined'}`;
222
+ if (hasValue(plist) || hasValue(config))
223
+ return makeItem(item, 'pass', `${item.label} is set.`, evidence);
224
+ return makeItem(item, 'info', `${item.label} is declared exempt (no non-exempt encryption) when you publish.`, evidence);
225
+ }
209
226
  function evaluateRestorePurchases(root, item) {
210
227
  const iap = scan(root, /react-native-purchases|expo-in-app-purchases|StoreKit/i);
211
228
  if (!iap.length)
@@ -242,14 +259,14 @@ function evaluateExpoLayout(root, item) {
242
259
  return makeItem(item, 'fail', 'Expo Router directories without _layout files found.', missing.slice(0, 8).join(', '));
243
260
  }
244
261
  function evaluateDuplicateRoutes(root, item) {
245
- const appDir = path.join(root, 'app');
262
+ const appDir = routerDir(root);
246
263
  if (!existsSync(appDir))
247
264
  return makeItem(item, 'pass', 'No Expo Router app directory found.', 'scan: app/ directory absent');
248
265
  const routes = new Map();
249
266
  for (const file of walkFiles(appDir)) {
250
267
  if (!/\.(tsx|ts|jsx|js)$/.test(file) || /(^|\/)_layout\.(tsx|ts|jsx|js)$/.test(toRel(root, file)))
251
268
  continue;
252
- const route = routePath(root, file);
269
+ const route = routePath(appDir, file);
253
270
  const current = routes.get(route) ?? [];
254
271
  current.push(toRel(root, file));
255
272
  routes.set(route, current);
@@ -289,7 +306,19 @@ function patternItem(root, item, regex, statusOnHit, messageOnHit, messageOnMiss
289
306
  return makeItem(item, statusOnHit, messageOnHit, evidence(matches));
290
307
  return makeItem(item, 'pass', messageOnMiss, `scan: no matches for ${regex}`);
291
308
  }
292
- const MICROPHONE_USE = /expo-av|AudioRecorder|requestRecordingPermissionsAsync|requestMicrophonePermissions/i;
309
+ const MICROPHONE_USE = /expo-av|AudioRecorder|requestRecordingPermissionsAsync|requestMicrophonePermissions|useMicrophonePermissions|recordAsync/i;
310
+ const CAMERA_USE = /expo-camera|CameraView|requestCameraPermissions|launchCameraAsync/i;
311
+ const LOCATION_USE = /expo-location|getCurrentPositionAsync|watchPositionAsync/i;
312
+ const ANDROID_PACKAGE_PERMISSIONS = [
313
+ { pkg: 'expo-camera', permissions: ['CAMERA'] },
314
+ { pkg: 'expo-camera', option: 'recordAudioAndroid', permissions: ['RECORD_AUDIO'] },
315
+ { pkg: 'expo-image-picker', permissions: ['CAMERA', 'READ_EXTERNAL_STORAGE'] },
316
+ { pkg: 'expo-image-picker', option: 'microphonePermission', permissions: ['RECORD_AUDIO'] },
317
+ { pkg: 'expo-audio', option: 'recordAudioAndroid', permissions: ['RECORD_AUDIO'] },
318
+ { pkg: 'expo-location', permissions: ['ACCESS_COARSE_LOCATION', 'ACCESS_FINE_LOCATION'] },
319
+ { pkg: 'expo-media-library', permissions: ['READ_EXTERNAL_STORAGE'] },
320
+ { pkg: 'expo-notifications', permissions: ['POST_NOTIFICATIONS'] },
321
+ ];
293
322
  function isFirebaseClientKey(root, match) {
294
323
  if (!/AIza[0-9A-Za-z_-]{30,}/.test(match.text))
295
324
  return false;
@@ -301,19 +330,36 @@ function isFirebaseClientKey(root, match) {
301
330
  }
302
331
  function missingPermissionRequirements(root, appJson) {
303
332
  const missing = [...missingIosUsageDescriptions(root, appJson)];
304
- const permissions = arrayOfStrings(getPath(appJson, ['expo', 'android', 'permissions']));
333
+ const bare = (permission) => permission.replace(/^android\.permission\./, '');
334
+ const blocked = new Set(arrayOfStrings(getPath(appJson, ['expo', 'android', 'blockedPermissions'])).map(bare));
335
+ const picker = configPluginOptions(appJson, 'expo-image-picker');
336
+ if (picker?.cameraPermission === false)
337
+ blocked.add('CAMERA');
338
+ if (picker?.microphonePermission === false)
339
+ blocked.add('RECORD_AUDIO');
340
+ const installed = installedPackages(root);
341
+ const granted = new Set(arrayOfStrings(getPath(appJson, ['expo', 'android', 'permissions'])).map(bare));
342
+ for (const { pkg, option, permissions } of ANDROID_PACKAGE_PERMISSIONS) {
343
+ const listed = configPluginOptions(appJson, pkg);
344
+ const auto = PLUGIN_PURPOSE_STRINGS[pkg]?.auto ?? false;
345
+ const adds = option === undefined
346
+ ? installed.has(pkg) || listed !== undefined
347
+ : listed ? listed[option] !== false : auto && installed.has(pkg);
348
+ if (adds)
349
+ permissions.forEach((permission) => granted.add(permission));
350
+ }
305
351
  const source = sourceText(root, ['app', 'src', 'components']);
306
352
  const androidChecks = [
307
- { name: 'camera', used: /expo-camera|CameraView|requestCameraPermissions/i, permissions: ['CAMERA'] },
308
- { name: 'location', used: /expo-location|getCurrentPositionAsync|watchPositionAsync/i, permissions: ['ACCESS_FINE_LOCATION', 'ACCESS_COARSE_LOCATION'] },
309
- { name: 'photos', used: /expo-image-picker|expo-media-library|MediaLibrary/i, permissions: ['READ_MEDIA_IMAGES', 'READ_EXTERNAL_STORAGE'] },
353
+ { name: 'camera', used: CAMERA_USE, permissions: ['CAMERA'] },
354
+ { name: 'location', used: LOCATION_USE, permissions: ['ACCESS_FINE_LOCATION', 'ACCESS_COARSE_LOCATION'] },
355
+ { name: 'photos', used: /expo-media-library|MediaLibrary/i, permissions: ['READ_MEDIA_IMAGES', 'READ_EXTERNAL_STORAGE'] },
310
356
  { name: 'microphone', used: MICROPHONE_USE, permissions: ['RECORD_AUDIO'] },
311
357
  { name: 'notifications', used: /expo-notifications/i, permissions: ['POST_NOTIFICATIONS'] },
312
358
  ];
313
359
  for (const check of androidChecks) {
314
360
  if (!check.used.test(source))
315
361
  continue;
316
- if (!check.permissions.some((permission) => permissions.includes(permission))) {
362
+ if (!check.permissions.some((permission) => granted.has(permission) && !blocked.has(permission))) {
317
363
  missing.push(`android ${check.name} permission (${check.permissions.join(' or ')})`);
318
364
  }
319
365
  }
@@ -323,13 +369,12 @@ function missingIosUsageDescriptions(root, appJson) {
323
369
  const infoPlist = objectValue(getPath(appJson, ['expo', 'ios', 'infoPlist']));
324
370
  const source = sourceText(root, ['app', 'src', 'components']);
325
371
  const checks = [
326
- { name: 'camera', used: /expo-camera|CameraView|requestCameraPermissions/i, keys: ['NSCameraUsageDescription'] },
327
- { name: 'location', used: /expo-location|getCurrentPositionAsync|watchPositionAsync/i, keys: ['NSLocationWhenInUseUsageDescription', 'NSLocationAlwaysAndWhenInUseUsageDescription'] },
372
+ { name: 'camera', used: CAMERA_USE, keys: ['NSCameraUsageDescription'] },
373
+ { name: 'location', used: LOCATION_USE, keys: ['NSLocationWhenInUseUsageDescription', 'NSLocationAlwaysAndWhenInUseUsageDescription'] },
328
374
  { name: 'photos', used: /expo-image-picker|expo-media-library|MediaLibrary/i, keys: ['NSPhotoLibraryUsageDescription'] },
329
375
  { name: 'microphone', used: MICROPHONE_USE, keys: ['NSMicrophoneUsageDescription'] },
330
376
  ];
331
- const pkg = readJson(path.join(root, 'package.json'));
332
- const installed = new Set(['dependencies', 'devDependencies'].flatMap((field) => Object.keys(objectValue(getPath(pkg, [field])))));
377
+ const installed = installedPackages(root);
333
378
  const missing = [];
334
379
  for (const check of checks) {
335
380
  if (!check.used.test(source))
@@ -370,6 +415,10 @@ function builtWithPurposeString(key, infoPlist, appJson, installed) {
370
415
  }
371
416
  return written;
372
417
  }
418
+ function installedPackages(root) {
419
+ const pkg = readJson(path.join(root, 'package.json'));
420
+ return new Set(['dependencies', 'devDependencies'].flatMap((field) => Object.keys(objectValue(getPath(pkg, [field])))));
421
+ }
373
422
  function configPluginOptions(appJson, name) {
374
423
  const plugins = getPath(appJson, ['expo', 'plugins']);
375
424
  if (!Array.isArray(plugins))
@@ -390,14 +439,10 @@ function unresolvedReason(item) {
390
439
  return `no deterministic evaluator is registered for ${item.check} item`;
391
440
  }
392
441
  function targetPlatformsFromAppJson(appJson) {
393
- const ios = stringValue(getPath(appJson, ['expo', 'ios', 'bundleIdentifier']));
394
- const android = stringValue(getPath(appJson, ['expo', 'android', 'package']));
395
- const platforms = [];
396
- if (ios)
397
- platforms.push('ios');
398
- if (android)
399
- platforms.push('android');
400
- return platforms.length ? platforms : ['ios', 'android'];
442
+ const declared = getPath(appJson, ['expo', 'platforms']);
443
+ if (!Array.isArray(declared))
444
+ return ['ios', 'android'];
445
+ return ['ios', 'android'].filter((platform) => declared.includes(platform));
401
446
  }
402
447
  function targetsItem(item, targetPlatforms) {
403
448
  return item.platform.some((platform) => targetPlatforms.includes(platform));
@@ -494,8 +539,12 @@ function sourceText(root, dirs) {
494
539
  function hasLayoutFile(dir) {
495
540
  return ['_layout.tsx', '_layout.ts', '_layout.jsx', '_layout.js'].some((name) => existsSync(path.join(dir, name)));
496
541
  }
497
- function routePath(root, file) {
498
- const rel = toRel(root, file).replace(/^app\//, '').replace(/\.(tsx|ts|jsx|js)$/, '');
542
+ function routerDir(root) {
543
+ const src = path.join(root, 'src', 'app');
544
+ return safeStat(src)?.isDirectory() ? src : path.join(root, 'app');
545
+ }
546
+ function routePath(appDir, file) {
547
+ const rel = toRel(appDir, file).replace(/\.(tsx|ts|jsx|js)$/, '');
499
548
  const parts = rel.split('/').filter((part) => part && !/^\(.+\)$/.test(part));
500
549
  if (parts[parts.length - 1] === 'index')
501
550
  parts.pop();
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.2.153';
1
+ export const CLI_VERSION = '0.2.155';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.2.153",
3
+ "version": "0.2.155",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
Binary file
Binary file