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.
- package/dist/commands/appStoreEvaluate.js +74 -25
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/runcloud-0.2.155.tgz +0 -0
- package/runcloud-0.2.153.tgz +0 -0
|
@@ -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: '
|
|
7
|
-
android_package: { path: ['expo', 'android', 'package'], absent: '
|
|
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 =
|
|
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(
|
|
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
|
|
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:
|
|
308
|
-
{ name: 'location', used:
|
|
309
|
-
{ name: 'photos', used: /expo-
|
|
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) =>
|
|
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:
|
|
327
|
-
{ name: 'location', used:
|
|
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
|
|
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
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
498
|
-
const
|
|
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.
|
|
1
|
+
export const CLI_VERSION = '0.2.155';
|
package/package.json
CHANGED
|
Binary file
|
package/runcloud-0.2.153.tgz
DELETED
|
Binary file
|