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.
- package/dist/commands/appStoreEvaluate.js +43 -40
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/runcloud-0.2.156.tgz +0 -0
- package/runcloud-0.2.154.tgz +0 -0
|
@@ -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: '
|
|
7
|
-
android_package: { path: ['expo', 'android', 'package'], absent: '
|
|
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,
|
|
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
|
|
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 =
|
|
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);
|
|
@@ -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
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
527
|
-
|
|
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(
|
|
530
|
-
const rel = toRel(
|
|
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.
|
|
1
|
+
export const CLI_VERSION = '0.2.156';
|
package/package.json
CHANGED
|
Binary file
|
package/runcloud-0.2.154.tgz
DELETED
|
Binary file
|