@newly/cli 0.2.203 → 0.2.205
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 +96 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync, } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
const PREVIEW_APP_ID = 'app.newly.preview';
|
|
4
5
|
const SCAFFOLD_SCHEME = 'expobase';
|
|
5
6
|
const INBOUND_LINK = /Linking\.createURL|makeRedirectUri|emailRedirectTo|\bredirectTo\s*:|expobase:\/\//;
|
|
7
|
+
const SCAFFOLD_APP_NAME = 'expo-base';
|
|
8
|
+
export const SCAFFOLD_ICON_SHA256 = '7a667804bb80a6a424a5daf18a2599c4f32237cf06fe78fc0de45dbb09e0eccf';
|
|
9
|
+
const SCAFFOLD_ICON_COMPOSER_MARK = path.join('Assets', 'expo-symbol 2.svg');
|
|
10
|
+
const STARTER_SCREEN = /@newly\/welcome|Welcome to Newly|Made with Newly/;
|
|
11
|
+
const PLACEHOLDER_ID = /com\.placeholder\./;
|
|
6
12
|
const PLACEHOLDER_COPY = /lorem ipsum|example\.com|\bTBD\b|\bTODO\b|\bplaceholder\b/i;
|
|
7
13
|
const INPUT_PLACEHOLDER = /\bplaceholder\??\s*[=:]\s*(\{[^}]*\}|"[^"]*"|'[^']*'|`[^`]*`|[\w.$]+)/gi;
|
|
8
14
|
const PLACEHOLDER_NAME = /\.placeholder\b|\bplaceholder\b(?=\s*[,})\]])/gi;
|
|
@@ -86,6 +92,7 @@ const TEXT_EXTENSIONS = new Set([
|
|
|
86
92
|
'.cc',
|
|
87
93
|
'.cpp',
|
|
88
94
|
'.cxx',
|
|
95
|
+
'.entitlements',
|
|
89
96
|
'.h',
|
|
90
97
|
'.hpp',
|
|
91
98
|
'.java',
|
|
@@ -113,6 +120,32 @@ const SKIP_DIRS = new Set([
|
|
|
113
120
|
'node_modules',
|
|
114
121
|
'Pods',
|
|
115
122
|
]);
|
|
123
|
+
export function withEvaluatorRows(manifest, rawItems, projectDir) {
|
|
124
|
+
const expoProject = existsSync(path.join(projectDir, 'app.json')) || dynamicExpoConfig(projectDir) !== undefined;
|
|
125
|
+
if (!Array.isArray(rawItems) || !expoProject)
|
|
126
|
+
return { items: rawItems, note: '' };
|
|
127
|
+
let settled;
|
|
128
|
+
try {
|
|
129
|
+
settled = new Map(evaluateCompliance(manifest, projectDir).items.map((row) => [row.id, row]));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return { items: rawItems, note: '' };
|
|
133
|
+
}
|
|
134
|
+
const corrected = [];
|
|
135
|
+
const items = rawItems.map((raw) => {
|
|
136
|
+
const sent = raw;
|
|
137
|
+
const row = typeof sent?.id === 'string' ? settled.get(sent.id) : undefined;
|
|
138
|
+
if (!row || row.status === 'info' || (row.status === 'pass' && row.id === 'version'))
|
|
139
|
+
return raw;
|
|
140
|
+
if (sent.status !== row.status)
|
|
141
|
+
corrected.push(`${row.id} is ${row.status}${row.message ? ` (${row.message})` : ''}`);
|
|
142
|
+
return { ...raw, ...row };
|
|
143
|
+
});
|
|
144
|
+
const note = corrected.length
|
|
145
|
+
? ` The code says otherwise on ${corrected.length} row(s), and the card shows the code's answer: ${corrected.join('; ')}. Describe these as the card shows them.`
|
|
146
|
+
: '';
|
|
147
|
+
return { items, note };
|
|
148
|
+
}
|
|
116
149
|
export function evaluateCompliance(manifest, projectDir, options = {}) {
|
|
117
150
|
const root = path.resolve(projectDir);
|
|
118
151
|
const appJson = asReleased(readJson(path.join(root, 'app.json')), options.iosBundleId);
|
|
@@ -216,6 +249,8 @@ function evaluateLocalItem(root, appJson, item) {
|
|
|
216
249
|
return makeItem(item, 'pass', 'Expo Router adds a layout where a directory has none.', 'expo-router getRoutesCore.js');
|
|
217
250
|
case 'expo_duplicate_route':
|
|
218
251
|
return evaluateDuplicateRoutes(root, item);
|
|
252
|
+
case 'starter_leftovers':
|
|
253
|
+
return evaluateStarterLeftovers(root, appJson, item);
|
|
219
254
|
case 'ios_encryption':
|
|
220
255
|
return evaluateEncryption(appJson, item);
|
|
221
256
|
case 'plugin_permission_missing':
|
|
@@ -297,6 +332,67 @@ function evaluateExternalPayment(root, item) {
|
|
|
297
332
|
return makeItem(item, 'pass', 'No external payment SDK patterns found.', 'scan: no Stripe/PayPal/Braintree patterns found');
|
|
298
333
|
return makeItem(item, 'warning', 'External payment SDK found; verify it is not used for digital goods on iOS.', evidence(matches));
|
|
299
334
|
}
|
|
335
|
+
function isScaffoldIconPng(root, icon) {
|
|
336
|
+
try {
|
|
337
|
+
return createHash('sha256').update(readFileSync(path.join(root, icon))).digest('hex') === SCAFFOLD_ICON_SHA256;
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function hasStarterIcon(root, appJson) {
|
|
344
|
+
const iosIcon = getPath(appJson, ['expo', 'ios', 'icon']);
|
|
345
|
+
if (iosIcon && typeof iosIcon === 'object') {
|
|
346
|
+
return Object.values(iosIcon).some((icon) => typeof icon === 'string' && isScaffoldIconPng(root, icon));
|
|
347
|
+
}
|
|
348
|
+
const iosPath = stringValue(iosIcon);
|
|
349
|
+
if (iosPath && !/\.png$/i.test(iosPath))
|
|
350
|
+
return existsSync(path.join(root, iosPath, SCAFFOLD_ICON_COMPOSER_MARK));
|
|
351
|
+
return isScaffoldIconPng(root, iosPath ?? stringValue(getPath(appJson, ['expo', 'icon'])) ?? './assets/images/icon.png');
|
|
352
|
+
}
|
|
353
|
+
function evaluateStarterLeftovers(root, appJson, item) {
|
|
354
|
+
const fromConfig = [];
|
|
355
|
+
const fromSource = [];
|
|
356
|
+
const proof = [];
|
|
357
|
+
if (stringValue(getPath(appJson, ['expo', 'name'])) === SCAFFOLD_APP_NAME) {
|
|
358
|
+
fromConfig.push(`the app is still named ${SCAFFOLD_APP_NAME}`);
|
|
359
|
+
proof.push(`app.json expo.name = "${SCAFFOLD_APP_NAME}"`);
|
|
360
|
+
}
|
|
361
|
+
const starterIcon = hasStarterIcon(root, appJson);
|
|
362
|
+
if (starterIcon)
|
|
363
|
+
proof.push('icon: the scaffold default');
|
|
364
|
+
const config = structuredClone(getPath(appJson, ['expo']) ?? {});
|
|
365
|
+
delete config.ios?.bundleIdentifier;
|
|
366
|
+
delete config.android?.package;
|
|
367
|
+
if (PLACEHOLDER_ID.test(JSON.stringify(config))) {
|
|
368
|
+
fromConfig.push('app.json still has a com.placeholder identifier');
|
|
369
|
+
proof.push('app.json: com.placeholder outside the store ids');
|
|
370
|
+
}
|
|
371
|
+
const screen = scan(root, STARTER_SCREEN, { dirs: ['app', 'src', 'components', 'ios'] });
|
|
372
|
+
if (screen.length) {
|
|
373
|
+
fromSource.push('the Newly welcome screen or branding is still in the app');
|
|
374
|
+
proof.push(evidence(screen));
|
|
375
|
+
}
|
|
376
|
+
const ids = scan(root, PLACEHOLDER_ID, { dirs: ['app', 'src', 'components', 'contexts', 'ios', 'plugins', 'targets'] });
|
|
377
|
+
if (ids.length) {
|
|
378
|
+
fromSource.push('a com.placeholder identifier is still in the code');
|
|
379
|
+
proof.push(evidence(ids));
|
|
380
|
+
}
|
|
381
|
+
const dynamicConfig = dynamicExpoConfig(root);
|
|
382
|
+
const blocking = dynamicConfig ? fromSource : [...fromConfig, ...fromSource];
|
|
383
|
+
const icon = starterIcon && !dynamicConfig ? ' The app icon is also still the starter icon.' : '';
|
|
384
|
+
const consequence = 'A reviewer reads the starter as an unfinished app (Guideline 2.1), and apps that look alike are rejected as spam (4.3), so replace it before you submit.';
|
|
385
|
+
if (blocking.length)
|
|
386
|
+
return makeItem(item, 'fail', `Left over from the Newly starter: ${blocking.join('; ')}.${icon} ${consequence}`, proof.join('\n'));
|
|
387
|
+
if (starterIcon && !dynamicConfig) {
|
|
388
|
+
return makeItem(item, 'warning', 'The app icon is still the starter icon, the same in every new Newly app. Give the app its own before you submit.', proof.join('\n'));
|
|
389
|
+
}
|
|
390
|
+
if (dynamicConfig && (fromConfig.length || starterIcon)) {
|
|
391
|
+
const found = [...fromConfig, ...(starterIcon ? ['the app icon is the starter icon'] : [])];
|
|
392
|
+
return makeItem(item, 'info', uncheckedUnder(dynamicConfig, `Left over from the Newly starter: ${found.join('; ')}.`), proof.join('\n'));
|
|
393
|
+
}
|
|
394
|
+
return makeItem(item, 'pass', 'Nothing left over from the Newly starter.', 'name, icon, welcome screen and placeholder ids checked');
|
|
395
|
+
}
|
|
300
396
|
function evaluateDuplicateRoutes(root, item) {
|
|
301
397
|
const appDir = routerDir(root);
|
|
302
398
|
if (!existsSync(appDir))
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.2.
|
|
1
|
+
export const CLI_VERSION = '0.2.205';
|