runcloud 0.2.154 → 0.2.156

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,21 @@
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';
4
+ const PLACEHOLDER_COPY = /lorem ipsum|example\.com|\bTBD\b|\bTODO\b|\bplaceholder\b/i;
5
+ const INPUT_PLACEHOLDER = /\bplaceholder\??\s*[=:]\s*(\{[^}]*\}|"[^"]*"|'[^']*'|`[^`]*`|[\w.$]+)/gi;
6
+ const PLACEHOLDER_NAME = /\.placeholder\b|\bplaceholder\b(?=\s*[,})\]])/gi;
7
+ function withoutInputPlaceholders(line) {
8
+ return line
9
+ .replace(INPUT_PLACEHOLDER, (_prop, value) => (/^["'`]/.test(value) ? value.replace(/example\.com/gi, '') : ''))
10
+ .replace(PLACEHOLDER_NAME, '');
11
+ }
3
12
  const CONFIG_RULES = {
4
13
  app_name: { path: ['expo', 'name'], absent: 'fail' },
5
14
  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' },
15
+ ios_bundle_id: { path: ['expo', 'ios', 'bundleIdentifier'], absent: 'info', storeId: /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$/ },
16
+ android_package: { path: ['expo', 'android', 'package'], absent: 'info', storeId: /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/ },
8
17
  version: { path: ['expo', 'version'], absent: 'warning' },
9
18
  scheme: { path: ['expo', 'scheme'], absent: 'info' },
10
- ios_encryption: { path: ['expo', 'ios', 'infoPlist', 'ITSAppUsesNonExemptEncryption'], absent: 'warning' },
11
19
  user_interface_style: { path: ['expo', 'userInterfaceStyle'], absent: 'info' },
12
20
  orientation: { path: ['expo', 'orientation'], absent: 'info' },
13
21
  ios_supports_tablet: { path: ['expo', 'ios', 'supportsTablet'], absent: 'info' },
@@ -157,7 +165,9 @@ function evaluateLocalItem(root, appJson, item) {
157
165
  case 'http_endpoints':
158
166
  return patternItem(root, item, /http:\/\/(?!localhost|127\.0\.0\.1)/i, 'warning', 'Non-local HTTP endpoint found.', 'No non-local HTTP endpoints found.');
159
167
  case 'placeholder_content':
160
- return patternItem(root, item, /lorem ipsum|example\.com|\bTBD\b|\bTODO\b|Coming soon|placeholder/i, 'warning', 'Placeholder text found.', 'No placeholder text found.');
168
+ return patternItem(root, item, PLACEHOLDER_COPY, 'warning', 'Placeholder text found.', 'No placeholder text found.', {
169
+ filter: (match) => PLACEHOLDER_COPY.test(withoutInputPlaceholders(match.text)),
170
+ });
161
171
  case 'competitor_reference':
162
172
  return patternItem(root, item, /google play|\.apk\b|samsung|android app/i, 'warning', 'Possible platform/competitor reference found in user-facing copy.', 'No competitor platform references found.', { dirs: ['app', 'src', 'components'] });
163
173
  case 'private_api_usage':
@@ -180,9 +190,11 @@ function evaluateLocalItem(root, appJson, item) {
180
190
  case 'external_payment_digital':
181
191
  return evaluateExternalPayment(root, item);
182
192
  case 'expo_layout_missing':
183
- return evaluateExpoLayout(root, item);
193
+ return makeItem(item, 'pass', 'Expo Router adds a layout where a directory has none.', 'expo-router getRoutesCore.js');
184
194
  case 'expo_duplicate_route':
185
195
  return evaluateDuplicateRoutes(root, item);
196
+ case 'ios_encryption':
197
+ return evaluateEncryption(appJson, item);
186
198
  case 'plugin_permission_missing':
187
199
  return evaluatePluginPermissions(root, appJson, item);
188
200
  case 'permission_descriptions':
@@ -202,10 +214,25 @@ function evaluateConfigItem(appJson, item, rule) {
202
214
  const value = getPath(appJson, rule.path);
203
215
  const present = hasValue(value);
204
216
  const evidence = `app.json ${rule.path.join('.')} = ${value === undefined ? 'undefined' : JSON.stringify(value)}`;
217
+ if (rule.storeId) {
218
+ const id = stringValue(value);
219
+ if (!id || id.toLowerCase() === PREVIEW_APP_ID)
220
+ return makeItem(item, rule.absent, `${item.label} is assigned when you publish.`, evidence);
221
+ if (!rule.storeId.test(id) || id.length > 255)
222
+ return makeItem(item, 'fail', `${item.label} is not valid. Use a reverse-domain ID like com.yourname.app.`, evidence);
223
+ }
205
224
  if (present)
206
225
  return makeItem(item, 'pass', `${item.label} is set.`, evidence);
207
226
  return makeItem(item, rule.absent, `${item.label} is not set.`, evidence);
208
227
  }
228
+ function evaluateEncryption(appJson, item) {
229
+ const plist = getPath(appJson, ['expo', 'ios', 'infoPlist', 'ITSAppUsesNonExemptEncryption']);
230
+ const config = getPath(appJson, ['expo', 'ios', 'config', 'usesNonExemptEncryption']);
231
+ const evidence = `app.json expo.ios.infoPlist.ITSAppUsesNonExemptEncryption = ${JSON.stringify(plist) ?? 'undefined'}, expo.ios.config.usesNonExemptEncryption = ${JSON.stringify(config) ?? 'undefined'}`;
232
+ if (hasValue(plist) || hasValue(config))
233
+ return makeItem(item, 'pass', `${item.label} is set.`, evidence);
234
+ return makeItem(item, 'info', `${item.label} is declared exempt (no non-exempt encryption) when you publish.`, evidence);
235
+ }
209
236
  function evaluateRestorePurchases(root, item) {
210
237
  const iap = scan(root, /react-native-purchases|expo-in-app-purchases|StoreKit/i);
211
238
  if (!iap.length)
@@ -231,25 +258,15 @@ function evaluateExternalPayment(root, item) {
231
258
  return makeItem(item, 'pass', 'No external payment SDK patterns found.', 'scan: no Stripe/PayPal/Braintree patterns found');
232
259
  return makeItem(item, 'warning', 'External payment SDK found; verify it is not used for digital goods on iOS.', evidence(matches));
233
260
  }
234
- function evaluateExpoLayout(root, item) {
235
- const appDir = path.join(root, 'app');
236
- if (!existsSync(appDir))
237
- return makeItem(item, 'pass', 'No Expo Router app directory found.', 'scan: app/ directory absent');
238
- const dirs = walkDirs(appDir).map((dir) => path.relative(root, dir) || 'app');
239
- const missing = dirs.filter((dir) => !hasLayoutFile(path.join(root, dir)));
240
- if (!missing.length)
241
- return makeItem(item, 'pass', 'Every app route directory has a layout file.', 'scan: app route directories');
242
- return makeItem(item, 'fail', 'Expo Router directories without _layout files found.', missing.slice(0, 8).join(', '));
243
- }
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);
@@ -422,14 +439,10 @@ function unresolvedReason(item) {
422
439
  return `no deterministic evaluator is registered for ${item.check} item`;
423
440
  }
424
441
  function targetPlatformsFromAppJson(appJson) {
425
- const ios = stringValue(getPath(appJson, ['expo', 'ios', 'bundleIdentifier']));
426
- const android = stringValue(getPath(appJson, ['expo', 'android', 'package']));
427
- const platforms = [];
428
- if (ios)
429
- platforms.push('ios');
430
- if (android)
431
- platforms.push('android');
432
- 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));
433
446
  }
434
447
  function targetsItem(item, targetPlatforms) {
435
448
  return item.platform.some((platform) => targetPlatforms.includes(platform));
@@ -503,17 +516,6 @@ function walkFiles(dir) {
503
516
  }
504
517
  return files;
505
518
  }
506
- function walkDirs(dir) {
507
- const dirs = [dir];
508
- for (const entry of safeReadDir(dir)) {
509
- if (SKIP_DIRS.has(entry.name))
510
- continue;
511
- const full = path.join(dir, entry.name);
512
- if (entry.isDirectory())
513
- dirs.push(...walkDirs(full));
514
- }
515
- return dirs;
516
- }
517
519
  function shouldScanFile(file) {
518
520
  return TEXT_EXTENSIONS.has(path.extname(file));
519
521
  }
@@ -523,11 +525,12 @@ function findByName(root, name) {
523
525
  function sourceText(root, dirs) {
524
526
  return collectScanFiles(root, dirs).map((file) => readText(file) ?? '').join('\n');
525
527
  }
526
- function hasLayoutFile(dir) {
527
- return ['_layout.tsx', '_layout.ts', '_layout.jsx', '_layout.js'].some((name) => existsSync(path.join(dir, name)));
528
+ function routerDir(root) {
529
+ const src = path.join(root, 'src', 'app');
530
+ return safeStat(src)?.isDirectory() ? src : path.join(root, 'app');
528
531
  }
529
- function routePath(root, file) {
530
- const rel = toRel(root, file).replace(/^app\//, '').replace(/\.(tsx|ts|jsx|js)$/, '');
532
+ function routePath(appDir, file) {
533
+ const rel = toRel(appDir, file).replace(/\.(tsx|ts|jsx|js)$/, '');
531
534
  const parts = rel.split('/').filter((part) => part && !/^\(.+\)$/.test(part));
532
535
  if (parts[parts.length - 1] === 'index')
533
536
  parts.pop();
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.2.154';
1
+ export const CLI_VERSION = '0.2.156';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.2.154",
3
+ "version": "0.2.156",
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