@forvibe/cli 1.1.1 → 2.0.1
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/index.js +933 -152
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -129,7 +129,7 @@ function findFiles(rootDir, extensions, maxDepth = 5) {
|
|
|
129
129
|
(f) => join(rootDir, f)
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
|
-
function
|
|
132
|
+
function readFileSafe2(filePath) {
|
|
133
133
|
try {
|
|
134
134
|
if (!existsSync(filePath)) return null;
|
|
135
135
|
const stat = statSync(filePath);
|
|
@@ -155,6 +155,45 @@ var DETECTION_RULES = [
|
|
|
155
155
|
return files.length >= 2 ? files : [];
|
|
156
156
|
}
|
|
157
157
|
},
|
|
158
|
+
{
|
|
159
|
+
stack: "expo",
|
|
160
|
+
label: "Expo (React Native)",
|
|
161
|
+
platforms: ["ios", "android"],
|
|
162
|
+
detect: (dir) => {
|
|
163
|
+
const files = [];
|
|
164
|
+
const pkgPath = join2(dir, "package.json");
|
|
165
|
+
const pkgContent = existsSync2(pkgPath) ? readFileSafe2(pkgPath) : null;
|
|
166
|
+
let hasExpoDep = false;
|
|
167
|
+
if (pkgContent) {
|
|
168
|
+
try {
|
|
169
|
+
const pkg = JSON.parse(pkgContent);
|
|
170
|
+
const allDeps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
171
|
+
if ("expo" in allDeps) {
|
|
172
|
+
hasExpoDep = true;
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
let hasExpoBlock = false;
|
|
178
|
+
const appJsonPath = join2(dir, "app.json");
|
|
179
|
+
if (existsSync2(appJsonPath)) {
|
|
180
|
+
const appJson = readFileSafe2(appJsonPath);
|
|
181
|
+
if (appJson && /"expo"\s*:\s*\{/.test(appJson)) {
|
|
182
|
+
hasExpoBlock = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const hasAppConfig = existsSync2(join2(dir, "app.config.js")) || existsSync2(join2(dir, "app.config.ts"));
|
|
186
|
+
const hasEas = existsSync2(join2(dir, "eas.json"));
|
|
187
|
+
if (hasExpoDep || hasExpoBlock || hasEas) {
|
|
188
|
+
if (pkgContent) files.push("package.json");
|
|
189
|
+
if (hasExpoBlock) files.push("app.json");
|
|
190
|
+
if (hasAppConfig) files.push(existsSync2(join2(dir, "app.config.ts")) ? "app.config.ts" : "app.config.js");
|
|
191
|
+
if (hasEas) files.push("eas.json");
|
|
192
|
+
if (existsSync2(join2(dir, "app"))) files.push("app/");
|
|
193
|
+
}
|
|
194
|
+
return files;
|
|
195
|
+
}
|
|
196
|
+
},
|
|
158
197
|
{
|
|
159
198
|
stack: "react-native",
|
|
160
199
|
label: "React Native",
|
|
@@ -163,7 +202,7 @@ var DETECTION_RULES = [
|
|
|
163
202
|
const files = [];
|
|
164
203
|
const pkgPath = join2(dir, "package.json");
|
|
165
204
|
if (existsSync2(pkgPath)) {
|
|
166
|
-
const content =
|
|
205
|
+
const content = readFileSafe2(pkgPath);
|
|
167
206
|
if (content && content.includes('"react-native"')) {
|
|
168
207
|
files.push("package.json");
|
|
169
208
|
if (existsSync2(join2(dir, "app.json"))) files.push("app.json");
|
|
@@ -255,8 +294,10 @@ var DETECTION_RULES = [
|
|
|
255
294
|
const entries = readdirSync2(dir);
|
|
256
295
|
for (const entry of entries) {
|
|
257
296
|
if (entry.endsWith(".csproj")) {
|
|
258
|
-
const content =
|
|
259
|
-
if (content
|
|
297
|
+
const content = readFileSafe2(join2(dir, entry));
|
|
298
|
+
if (!content) continue;
|
|
299
|
+
const isMaui = /<UseMaui>\s*true\s*<\/UseMaui>/i.test(content) || /Microsoft\.Maui\.Controls/.test(content) || /Microsoft\.Maui\.Sdk/.test(content) || /-android|-ios|-maccatalyst/.test(content);
|
|
300
|
+
if (isMaui) {
|
|
260
301
|
files.push(entry);
|
|
261
302
|
}
|
|
262
303
|
}
|
|
@@ -264,6 +305,7 @@ var DETECTION_RULES = [
|
|
|
264
305
|
} catch {
|
|
265
306
|
}
|
|
266
307
|
if (existsSync2(join2(dir, "Platforms"))) files.push("Platforms/");
|
|
308
|
+
if (existsSync2(join2(dir, "Resources"))) files.push("Resources/");
|
|
267
309
|
return files.length >= 1 ? files : [];
|
|
268
310
|
}
|
|
269
311
|
}
|
|
@@ -297,12 +339,13 @@ function parseConfig(rootDir, techStack) {
|
|
|
297
339
|
switch (techStack) {
|
|
298
340
|
case "flutter":
|
|
299
341
|
return parseFlutterConfig(rootDir);
|
|
342
|
+
case "expo":
|
|
343
|
+
return parseExpoConfig(rootDir);
|
|
300
344
|
case "react-native":
|
|
301
345
|
return parseReactNativeConfig(rootDir);
|
|
302
346
|
case "swift":
|
|
303
347
|
return parseSwiftConfig(rootDir);
|
|
304
348
|
case "kotlin":
|
|
305
|
-
case "java":
|
|
306
349
|
return parseAndroidConfig(rootDir);
|
|
307
350
|
case "capacitor":
|
|
308
351
|
return parseCapacitorConfig(rootDir);
|
|
@@ -324,7 +367,7 @@ function emptyConfig() {
|
|
|
324
367
|
}
|
|
325
368
|
function parseFlutterConfig(rootDir) {
|
|
326
369
|
const config = emptyConfig();
|
|
327
|
-
const pubspecContent =
|
|
370
|
+
const pubspecContent = readFileSafe2(join3(rootDir, "pubspec.yaml"));
|
|
328
371
|
if (pubspecContent) {
|
|
329
372
|
try {
|
|
330
373
|
const pubspec = YAML.parse(pubspecContent);
|
|
@@ -349,7 +392,7 @@ function parseFlutterConfig(rootDir) {
|
|
|
349
392
|
if (!config.bundle_id || config.bundle_id.includes("$(")) {
|
|
350
393
|
const pbxprojPath = findFile(join3(rootDir, "ios"), "project.pbxproj", 4);
|
|
351
394
|
if (pbxprojPath) {
|
|
352
|
-
const content =
|
|
395
|
+
const content = readFileSafe2(pbxprojPath);
|
|
353
396
|
if (content) {
|
|
354
397
|
const bundleMatch = content.match(
|
|
355
398
|
/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*"?([^";]+)"?/
|
|
@@ -370,9 +413,85 @@ function parseFlutterConfig(rootDir) {
|
|
|
370
413
|
}
|
|
371
414
|
return config;
|
|
372
415
|
}
|
|
416
|
+
function parseExpoConfig(rootDir) {
|
|
417
|
+
const config = emptyConfig();
|
|
418
|
+
const appJsonContent = readFileSafe2(join3(rootDir, "app.json"));
|
|
419
|
+
if (appJsonContent) {
|
|
420
|
+
try {
|
|
421
|
+
const appJson = JSON.parse(appJsonContent);
|
|
422
|
+
const expo = appJson.expo || appJson;
|
|
423
|
+
config.app_name = expo.name || appJson.displayName || appJson.name || null;
|
|
424
|
+
config.version = expo.version || null;
|
|
425
|
+
config.bundle_id = expo.ios?.bundleIdentifier || expo.android?.package || null;
|
|
426
|
+
config.description = expo.description || null;
|
|
427
|
+
config.min_ios_version = expo.ios?.deploymentTarget || null;
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (!config.app_name || !config.bundle_id || !config.version) {
|
|
432
|
+
for (const name of ["app.config.ts", "app.config.js"]) {
|
|
433
|
+
const content = readFileSafe2(join3(rootDir, name));
|
|
434
|
+
if (!content) continue;
|
|
435
|
+
if (!config.app_name) {
|
|
436
|
+
const m = content.match(/name\s*:\s*['"]([^'"]+)['"]/);
|
|
437
|
+
if (m) config.app_name = m[1];
|
|
438
|
+
}
|
|
439
|
+
if (!config.version) {
|
|
440
|
+
const m = content.match(/version\s*:\s*['"]([^'"]+)['"]/);
|
|
441
|
+
if (m) config.version = m[1];
|
|
442
|
+
}
|
|
443
|
+
if (!config.bundle_id) {
|
|
444
|
+
const m = content.match(/bundleIdentifier\s*:\s*['"]([^'"]+)['"]/) || content.match(/package\s*:\s*['"]([^'"]+)['"]/);
|
|
445
|
+
if (m) config.bundle_id = m[1];
|
|
446
|
+
}
|
|
447
|
+
if (!config.description) {
|
|
448
|
+
const m = content.match(/description\s*:\s*['"]([^'"]+)['"]/);
|
|
449
|
+
if (m) config.description = m[1];
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const pkgContent = readFileSafe2(join3(rootDir, "package.json"));
|
|
454
|
+
if (pkgContent) {
|
|
455
|
+
try {
|
|
456
|
+
const pkg = JSON.parse(pkgContent);
|
|
457
|
+
if (!config.version) config.version = pkg.version || null;
|
|
458
|
+
if (!config.description) config.description = pkg.description || null;
|
|
459
|
+
if (!config.app_name) config.app_name = pkg.name || null;
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (!config.bundle_id || !config.app_name) {
|
|
464
|
+
const infoPlistPath = findFile(join3(rootDir, "ios"), "Info.plist", 4);
|
|
465
|
+
if (infoPlistPath) {
|
|
466
|
+
const plistData = parsePlist(infoPlistPath);
|
|
467
|
+
if (plistData) {
|
|
468
|
+
if (!config.bundle_id) {
|
|
469
|
+
const id = plistData.CFBundleIdentifier;
|
|
470
|
+
if (id && !isXcodeVariable(id)) config.bundle_id = id;
|
|
471
|
+
}
|
|
472
|
+
if (!config.app_name) {
|
|
473
|
+
const display = plistData.CFBundleDisplayName || plistData.CFBundleName;
|
|
474
|
+
if (display && !isXcodeVariable(display)) config.app_name = display;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (!config.bundle_id) {
|
|
480
|
+
const buildGradlePath = findFile(join3(rootDir, "android/app"), "build.gradle", 2) || findFile(join3(rootDir, "android/app"), "build.gradle.kts", 2);
|
|
481
|
+
if (buildGradlePath) {
|
|
482
|
+
const gradleConfig = parseGradle(buildGradlePath);
|
|
483
|
+
if (gradleConfig.applicationId) config.bundle_id = gradleConfig.applicationId;
|
|
484
|
+
if (!config.min_android_sdk) config.min_android_sdk = gradleConfig.minSdk || null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (!config.app_name) {
|
|
488
|
+
config.app_name = readAndroidAppName(join3(rootDir, "android"));
|
|
489
|
+
}
|
|
490
|
+
return config;
|
|
491
|
+
}
|
|
373
492
|
function parseReactNativeConfig(rootDir) {
|
|
374
493
|
const config = emptyConfig();
|
|
375
|
-
const appJsonContent =
|
|
494
|
+
const appJsonContent = readFileSafe2(join3(rootDir, "app.json"));
|
|
376
495
|
if (appJsonContent) {
|
|
377
496
|
try {
|
|
378
497
|
const appJson = JSON.parse(appJsonContent);
|
|
@@ -380,7 +499,18 @@ function parseReactNativeConfig(rootDir) {
|
|
|
380
499
|
} catch {
|
|
381
500
|
}
|
|
382
501
|
}
|
|
383
|
-
|
|
502
|
+
if (!config.app_name) {
|
|
503
|
+
for (const name of ["app.config.ts", "app.config.js"]) {
|
|
504
|
+
const content = readFileSafe2(join3(rootDir, name));
|
|
505
|
+
if (!content) continue;
|
|
506
|
+
const m = content.match(/name\s*:\s*['"]([^'"]+)['"]/);
|
|
507
|
+
if (m) {
|
|
508
|
+
config.app_name = m[1];
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const pkgContent = readFileSafe2(join3(rootDir, "package.json"));
|
|
384
514
|
if (pkgContent) {
|
|
385
515
|
try {
|
|
386
516
|
const pkg = JSON.parse(pkgContent);
|
|
@@ -396,12 +526,18 @@ function parseReactNativeConfig(rootDir) {
|
|
|
396
526
|
if (plistData) {
|
|
397
527
|
config.bundle_id = plistData.CFBundleIdentifier || null;
|
|
398
528
|
config.min_ios_version = plistData.MinimumOSVersion || null;
|
|
529
|
+
if (!config.app_name) {
|
|
530
|
+
const display = plistData.CFBundleDisplayName || plistData.CFBundleName;
|
|
531
|
+
if (display && !isXcodeVariable(display)) {
|
|
532
|
+
config.app_name = display;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
399
535
|
}
|
|
400
536
|
}
|
|
401
537
|
if (!config.bundle_id || config.bundle_id.includes("$(")) {
|
|
402
538
|
const pbxprojPath = findFile(join3(rootDir, "ios"), "project.pbxproj", 4);
|
|
403
539
|
if (pbxprojPath) {
|
|
404
|
-
const content =
|
|
540
|
+
const content = readFileSafe2(pbxprojPath);
|
|
405
541
|
if (content) {
|
|
406
542
|
const bundleMatch = content.match(
|
|
407
543
|
/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*"?([^";]+)"?/
|
|
@@ -420,6 +556,9 @@ function parseReactNativeConfig(rootDir) {
|
|
|
420
556
|
}
|
|
421
557
|
config.min_android_sdk = gradleConfig.minSdk || null;
|
|
422
558
|
}
|
|
559
|
+
if (!config.app_name) {
|
|
560
|
+
config.app_name = readAndroidAppName(join3(rootDir, "android"));
|
|
561
|
+
}
|
|
423
562
|
return config;
|
|
424
563
|
}
|
|
425
564
|
var EXTENSION_SUFFIXES = [
|
|
@@ -510,7 +649,7 @@ function parseSwiftConfig(rootDir) {
|
|
|
510
649
|
const config = emptyConfig();
|
|
511
650
|
const pbxprojPath = findFile(rootDir, "project.pbxproj", 5);
|
|
512
651
|
if (pbxprojPath) {
|
|
513
|
-
const content =
|
|
652
|
+
const content = readFileSafe2(pbxprojPath);
|
|
514
653
|
if (content) {
|
|
515
654
|
const pbx = parsePbxproj(content);
|
|
516
655
|
config.bundle_id = pbx.bundleId;
|
|
@@ -562,34 +701,26 @@ function parseSwiftConfig(rootDir) {
|
|
|
562
701
|
}
|
|
563
702
|
function parseAndroidConfig(rootDir) {
|
|
564
703
|
const config = emptyConfig();
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
704
|
+
const candidates = [];
|
|
705
|
+
const addCandidate = (p) => {
|
|
706
|
+
if (p) candidates.push(p);
|
|
707
|
+
};
|
|
708
|
+
addCandidate(findFile(join3(rootDir, "app"), "build.gradle", 2));
|
|
709
|
+
addCandidate(findFile(join3(rootDir, "app"), "build.gradle.kts", 2));
|
|
710
|
+
addCandidate(findFile(rootDir, "build.gradle", 1));
|
|
711
|
+
addCandidate(findFile(rootDir, "build.gradle.kts", 1));
|
|
712
|
+
for (const f of findFiles(rootDir, ["build.gradle", "build.gradle.kts"], 4)) {
|
|
713
|
+
if (!candidates.includes(f)) candidates.push(f);
|
|
714
|
+
}
|
|
715
|
+
for (const path2 of candidates) {
|
|
716
|
+
const g = parseGradle(path2);
|
|
717
|
+
if (!config.bundle_id && g.applicationId) config.bundle_id = g.applicationId;
|
|
718
|
+
if (!config.version && g.versionName) config.version = g.versionName;
|
|
719
|
+
if (!config.min_android_sdk && g.minSdk) config.min_android_sdk = g.minSdk;
|
|
720
|
+
if (config.bundle_id && config.version && config.min_android_sdk) break;
|
|
581
721
|
}
|
|
582
722
|
if (!config.app_name) {
|
|
583
|
-
|
|
584
|
-
if (stringsPath) {
|
|
585
|
-
const content = readFileSafe(stringsPath);
|
|
586
|
-
if (content) {
|
|
587
|
-
const nameMatch = content.match(
|
|
588
|
-
/<string name="app_name">(.*?)<\/string>/
|
|
589
|
-
);
|
|
590
|
-
if (nameMatch) config.app_name = nameMatch[1];
|
|
591
|
-
}
|
|
592
|
-
}
|
|
723
|
+
config.app_name = readAndroidAppName(rootDir);
|
|
593
724
|
}
|
|
594
725
|
return config;
|
|
595
726
|
}
|
|
@@ -600,7 +731,7 @@ function parseCapacitorConfig(rootDir) {
|
|
|
600
731
|
"capacitor.config.ts",
|
|
601
732
|
"capacitor.config.js"
|
|
602
733
|
]) {
|
|
603
|
-
const content =
|
|
734
|
+
const content = readFileSafe2(join3(rootDir, name));
|
|
604
735
|
if (content) {
|
|
605
736
|
if (name.endsWith(".json")) {
|
|
606
737
|
try {
|
|
@@ -618,7 +749,7 @@ function parseCapacitorConfig(rootDir) {
|
|
|
618
749
|
break;
|
|
619
750
|
}
|
|
620
751
|
}
|
|
621
|
-
const pkgContent =
|
|
752
|
+
const pkgContent = readFileSafe2(join3(rootDir, "package.json"));
|
|
622
753
|
if (pkgContent) {
|
|
623
754
|
try {
|
|
624
755
|
const pkg = JSON.parse(pkgContent);
|
|
@@ -635,7 +766,7 @@ function parseMauiConfig(rootDir) {
|
|
|
635
766
|
const entries = readdirSync3(rootDir);
|
|
636
767
|
for (const entry of entries) {
|
|
637
768
|
if (entry.endsWith(".csproj")) {
|
|
638
|
-
const content =
|
|
769
|
+
const content = readFileSafe2(join3(rootDir, entry));
|
|
639
770
|
if (content) {
|
|
640
771
|
const appIdMatch = content.match(
|
|
641
772
|
/<ApplicationId>(.*?)<\/ApplicationId>/
|
|
@@ -658,7 +789,7 @@ function parseMauiConfig(rootDir) {
|
|
|
658
789
|
return config;
|
|
659
790
|
}
|
|
660
791
|
function parsePlist(filePath) {
|
|
661
|
-
const content =
|
|
792
|
+
const content = readFileSafe2(filePath);
|
|
662
793
|
if (!content) return null;
|
|
663
794
|
try {
|
|
664
795
|
return plist.parse(content);
|
|
@@ -666,8 +797,40 @@ function parsePlist(filePath) {
|
|
|
666
797
|
return null;
|
|
667
798
|
}
|
|
668
799
|
}
|
|
800
|
+
function lookupAndroidString(searchDir, stringName) {
|
|
801
|
+
const stringsPath = findFile(searchDir, "strings.xml", 8);
|
|
802
|
+
if (!stringsPath) return null;
|
|
803
|
+
const content = readFileSafe2(stringsPath);
|
|
804
|
+
if (!content) return null;
|
|
805
|
+
const escaped = stringName.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
806
|
+
const pattern = new RegExp(
|
|
807
|
+
`<string\\s+name="${escaped}"[^>]*>([\\s\\S]*?)<\\/string>`
|
|
808
|
+
);
|
|
809
|
+
const m = content.match(pattern);
|
|
810
|
+
return m ? m[1].trim() : null;
|
|
811
|
+
}
|
|
812
|
+
function readAndroidAppName(searchDir) {
|
|
813
|
+
const manifestPath = findFile(searchDir, "AndroidManifest.xml", 6);
|
|
814
|
+
if (manifestPath) {
|
|
815
|
+
const content = readFileSafe2(manifestPath);
|
|
816
|
+
if (content) {
|
|
817
|
+
const labelMatch = content.match(/android:label\s*=\s*"([^"]+)"/);
|
|
818
|
+
if (labelMatch) {
|
|
819
|
+
const label = labelMatch[1];
|
|
820
|
+
const stringRef = label.match(/^@string\/(.+)$/);
|
|
821
|
+
if (stringRef) {
|
|
822
|
+
const resolved = lookupAndroidString(searchDir, stringRef[1]);
|
|
823
|
+
if (resolved) return resolved;
|
|
824
|
+
} else if (!label.startsWith("@")) {
|
|
825
|
+
return label;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return lookupAndroidString(searchDir, "app_name");
|
|
831
|
+
}
|
|
669
832
|
function parseGradle(filePath) {
|
|
670
|
-
const content =
|
|
833
|
+
const content = readFileSafe2(filePath);
|
|
671
834
|
if (!content) return { applicationId: null, versionName: null, minSdk: null };
|
|
672
835
|
const appIdMatch = content.match(
|
|
673
836
|
/applicationId\s*[=:]\s*['"]([^'"]+)['"]/
|
|
@@ -687,6 +850,7 @@ function parseGradle(filePath) {
|
|
|
687
850
|
|
|
688
851
|
// src/analyzers/sdk-scanner.ts
|
|
689
852
|
import { join as join4 } from "path";
|
|
853
|
+
import { readdirSync as readdirSync4 } from "fs";
|
|
690
854
|
import YAML2 from "yaml";
|
|
691
855
|
var SDK_MAP = {
|
|
692
856
|
// Analytics
|
|
@@ -791,6 +955,45 @@ var SDK_MAP = {
|
|
|
791
955
|
ARKit: { category: "other", collects: [] },
|
|
792
956
|
RealityKit: { category: "other", collects: [] },
|
|
793
957
|
EventKit: { category: "other", collects: [] },
|
|
958
|
+
// --- Expo SDK packages ---
|
|
959
|
+
"expo-notifications": { category: "other", collects: ["device_info"] },
|
|
960
|
+
"expo-tracking-transparency": { category: "ads", collects: ["device_info"], advertising: "personalized" },
|
|
961
|
+
"expo-location": { category: "other", collects: ["location"] },
|
|
962
|
+
"expo-contacts": { category: "other", collects: ["contacts"] },
|
|
963
|
+
"expo-camera": { category: "other", collects: ["photos_media"] },
|
|
964
|
+
"expo-image-picker": { category: "other", collects: ["photos_media"] },
|
|
965
|
+
"expo-media-library": { category: "other", collects: ["photos_media"] },
|
|
966
|
+
"expo-auth-session": { category: "auth", collects: ["name_email"] },
|
|
967
|
+
"expo-apple-authentication": { category: "auth", collects: ["name_email"] },
|
|
968
|
+
"expo-google-sign-in": { category: "auth", collects: ["name_email", "profile_photo"] },
|
|
969
|
+
"expo-in-app-purchases": { category: "payment", collects: ["financial_info"] },
|
|
970
|
+
"expo-ads-admob": { category: "ads", collects: ["device_info"], advertising: "personalized" },
|
|
971
|
+
"expo-firebase-analytics": { category: "analytics", collects: ["usage_analytics", "device_info"] },
|
|
972
|
+
"expo-local-authentication": { category: "auth", collects: ["biometric_data"] },
|
|
973
|
+
"expo-health-connect": { category: "other", collects: ["health_data"] },
|
|
974
|
+
"expo-sensors": { category: "other", collects: ["device_info"] },
|
|
975
|
+
"expo-device": { category: "other", collects: ["device_info"] },
|
|
976
|
+
// --- .NET MAUI packages ---
|
|
977
|
+
"Microsoft.AppCenter.Analytics": { category: "analytics", collects: ["usage_analytics", "device_info"] },
|
|
978
|
+
"Microsoft.AppCenter.Crashes": { category: "analytics", collects: ["device_info"] },
|
|
979
|
+
"Sentry.Maui": { category: "analytics", collects: ["device_info"] },
|
|
980
|
+
"Plugin.Firebase": { category: "analytics", collects: ["usage_analytics", "device_info"] },
|
|
981
|
+
"Plugin.Firebase.Analytics": { category: "analytics", collects: ["usage_analytics", "device_info"] },
|
|
982
|
+
"Plugin.Firebase.Auth": { category: "auth", collects: ["name_email"] },
|
|
983
|
+
"Plugin.Firebase.CloudMessaging": { category: "other", collects: ["device_info"] },
|
|
984
|
+
"Plugin.GoogleClient": { category: "auth", collects: ["name_email", "profile_photo"] },
|
|
985
|
+
"Plugin.SignInWithApple": { category: "auth", collects: ["name_email"] },
|
|
986
|
+
"Plugin.InAppBilling": { category: "payment", collects: ["financial_info"] },
|
|
987
|
+
"CommunityToolkit.Maui": { category: "other", collects: [] },
|
|
988
|
+
"CommunityToolkit.Maui.MediaElement": { category: "other", collects: ["photos_media"] },
|
|
989
|
+
"Plugin.Maui.Audio": { category: "other", collects: ["photos_media"] },
|
|
990
|
+
"Xamarin.Google.Android.Play.Review": { category: "other", collects: [] },
|
|
991
|
+
"Plugin.LocalNotification": { category: "other", collects: [] },
|
|
992
|
+
"Plugin.Maui.Biometric": { category: "auth", collects: ["biometric_data"] },
|
|
993
|
+
"Plugin.Permissions": { category: "other", collects: [] },
|
|
994
|
+
"Mapsui.Maui": { category: "other", collects: ["location"] },
|
|
995
|
+
"Plugin.Maui.AppRating": { category: "other", collects: [] },
|
|
996
|
+
"Plugin.Geolocator": { category: "other", collects: ["location"] },
|
|
794
997
|
// --- Native Android frameworks (detected via `import` in Kotlin/Java source) ---
|
|
795
998
|
"com.google.android.gms.ads": { category: "ads", collects: ["device_info"], advertising: "personalized" },
|
|
796
999
|
"com.google.android.gms.maps": { category: "other", collects: ["location"] },
|
|
@@ -799,8 +1002,123 @@ var SDK_MAP = {
|
|
|
799
1002
|
"com.android.billingclient": { category: "payment", collects: ["financial_info"] },
|
|
800
1003
|
"androidx.health": { category: "other", collects: ["health_data"] },
|
|
801
1004
|
"androidx.biometric": { category: "auth", collects: ["biometric_data"] },
|
|
802
|
-
"androidx.camera": { category: "other", collects: ["photos_media"] }
|
|
1005
|
+
"androidx.camera": { category: "other", collects: ["photos_media"] },
|
|
1006
|
+
"androidx.compose": { category: "other", collects: [] },
|
|
1007
|
+
"androidx.navigation.compose": { category: "other", collects: [] },
|
|
1008
|
+
// --- Android artifact-name keys (unique enough that bare artifact name is safe) ---
|
|
1009
|
+
"play-services-ads": { category: "ads", collects: ["device_info"], advertising: "personalized" },
|
|
1010
|
+
"play-services-ads-lite": { category: "ads", collects: ["device_info"], advertising: "personalized" },
|
|
1011
|
+
"play-services-maps": { category: "other", collects: ["location"] },
|
|
1012
|
+
"play-services-location": { category: "other", collects: ["location"] },
|
|
1013
|
+
"play-services-auth": { category: "auth", collects: ["name_email", "profile_photo"] },
|
|
1014
|
+
"play-services-auth-api-phone": { category: "auth", collects: ["name_email"] },
|
|
1015
|
+
billing: { category: "payment", collects: ["financial_info"] },
|
|
1016
|
+
"billing-ktx": { category: "payment", collects: ["financial_info"] },
|
|
1017
|
+
biometric: { category: "auth", collects: ["biometric_data"] },
|
|
1018
|
+
"biometric-ktx": { category: "auth", collects: ["biometric_data"] },
|
|
1019
|
+
"health-connect-client": { category: "other", collects: ["health_data"] },
|
|
1020
|
+
"camera-core": { category: "other", collects: ["photos_media"] },
|
|
1021
|
+
"camera-camera2": { category: "other", collects: ["photos_media"] },
|
|
1022
|
+
"camera-lifecycle": { category: "other", collects: ["photos_media"] },
|
|
1023
|
+
"camera-view": { category: "other", collects: ["photos_media"] },
|
|
1024
|
+
"camera-extensions": { category: "other", collects: ["photos_media"] },
|
|
1025
|
+
// firebase-analytics / firebase-auth already declared above (Flutter dep names)
|
|
1026
|
+
"firebase-analytics-ktx": { category: "analytics", collects: ["usage_analytics", "device_info"] },
|
|
1027
|
+
"firebase-auth-ktx": { category: "auth", collects: ["name_email"] },
|
|
1028
|
+
"firebase-crashlytics": { category: "analytics", collects: ["device_info"] },
|
|
1029
|
+
"firebase-crashlytics-ktx": { category: "analytics", collects: ["device_info"] },
|
|
1030
|
+
"firebase-messaging": { category: "other", collects: ["device_info"] },
|
|
1031
|
+
"firebase-messaging-ktx": { category: "other", collects: ["device_info"] },
|
|
1032
|
+
"firebase-firestore": { category: "cloud", collects: [] },
|
|
1033
|
+
"firebase-firestore-ktx": { category: "cloud", collects: [] },
|
|
1034
|
+
"firebase-storage": { category: "cloud", collects: [] },
|
|
1035
|
+
"firebase-storage-ktx": { category: "cloud", collects: [] },
|
|
1036
|
+
"firebase-database": { category: "cloud", collects: [] },
|
|
1037
|
+
"firebase-database-ktx": { category: "cloud", collects: [] },
|
|
1038
|
+
"firebase-config": { category: "cloud", collects: [] },
|
|
1039
|
+
"firebase-config-ktx": { category: "cloud", collects: [] },
|
|
1040
|
+
"firebase-functions": { category: "cloud", collects: [] },
|
|
1041
|
+
"firebase-functions-ktx": { category: "cloud", collects: [] },
|
|
1042
|
+
"hilt-android": { category: "other", collects: [] },
|
|
1043
|
+
"hilt-navigation-compose": { category: "other", collects: [] },
|
|
1044
|
+
"koin-android": { category: "other", collects: [] },
|
|
1045
|
+
"koin-androidx-compose": { category: "other", collects: [] },
|
|
1046
|
+
"room-runtime": { category: "other", collects: [] },
|
|
1047
|
+
"room-ktx": { category: "other", collects: [] },
|
|
1048
|
+
"room-compiler": { category: "other", collects: [] },
|
|
1049
|
+
retrofit: { category: "other", collects: [] },
|
|
1050
|
+
"retrofit-converter-gson": { category: "other", collects: [] },
|
|
1051
|
+
"retrofit-converter-moshi": { category: "other", collects: [] },
|
|
1052
|
+
okhttp: { category: "other", collects: [] },
|
|
1053
|
+
"okhttp-logging-interceptor": { category: "other", collects: [] },
|
|
1054
|
+
"coil-compose": { category: "other", collects: [] },
|
|
1055
|
+
coil: { category: "other", collects: [] },
|
|
1056
|
+
glide: { category: "other", collects: [] },
|
|
1057
|
+
"navigation-compose": { category: "other", collects: [] },
|
|
1058
|
+
"activity-compose": { category: "other", collects: [] },
|
|
1059
|
+
"lifecycle-viewmodel-compose": { category: "other", collects: [] },
|
|
1060
|
+
"lifecycle-runtime-compose": { category: "other", collects: [] },
|
|
1061
|
+
"work-runtime": { category: "other", collects: [] },
|
|
1062
|
+
"work-runtime-ktx": { category: "other", collects: [] },
|
|
1063
|
+
"datastore-preferences": { category: "other", collects: [] },
|
|
1064
|
+
"kotlinx-coroutines-android": { category: "other", collects: [] },
|
|
1065
|
+
// --- group:artifact keys for Compose (artifact names alone are ambiguous: "ui", "material3") ---
|
|
1066
|
+
"androidx.compose.ui:ui": { category: "other", collects: [] },
|
|
1067
|
+
"androidx.compose.ui:ui-tooling": { category: "other", collects: [] },
|
|
1068
|
+
"androidx.compose.ui:ui-tooling-preview": { category: "other", collects: [] },
|
|
1069
|
+
"androidx.compose.material3:material3": { category: "other", collects: [] },
|
|
1070
|
+
"androidx.compose.material:material": { category: "other", collects: [] },
|
|
1071
|
+
"androidx.compose.material:material-icons-extended": { category: "other", collects: [] },
|
|
1072
|
+
"androidx.compose.foundation:foundation": { category: "other", collects: [] },
|
|
1073
|
+
"androidx.compose.runtime:runtime": { category: "other", collects: [] },
|
|
1074
|
+
"androidx.compose.animation:animation": { category: "other", collects: [] },
|
|
1075
|
+
// --- Popular third-party Android SDKs (group:artifact keys) ---
|
|
1076
|
+
"com.revenuecat.purchases:purchases": { category: "payment", collects: ["financial_info"] },
|
|
1077
|
+
"com.adjust.sdk:adjust-android": { category: "analytics", collects: ["usage_analytics", "device_info"] },
|
|
1078
|
+
"com.appsflyer:af-android-sdk": { category: "analytics", collects: ["usage_analytics", "device_info"] },
|
|
1079
|
+
"io.sentry:sentry-android": { category: "analytics", collects: ["device_info"] },
|
|
1080
|
+
"com.amplitude:android-sdk": { category: "analytics", collects: ["usage_analytics"] },
|
|
1081
|
+
"com.mixpanel.android:mixpanel-android": { category: "analytics", collects: ["usage_analytics"] },
|
|
1082
|
+
"com.posthog:posthog-android": { category: "analytics", collects: ["usage_analytics"] },
|
|
1083
|
+
"com.stripe:stripe-android": { category: "payment", collects: ["financial_info"] }
|
|
803
1084
|
};
|
|
1085
|
+
function parseVersionCatalog(content, catalog) {
|
|
1086
|
+
const idx = content.indexOf("[libraries]");
|
|
1087
|
+
if (idx < 0) return;
|
|
1088
|
+
const after = content.slice(idx + "[libraries]".length);
|
|
1089
|
+
const nextSection = after.search(/\n\s*\[[^\]]+\]/);
|
|
1090
|
+
const block = nextSection >= 0 ? after.slice(0, nextSection) : after;
|
|
1091
|
+
for (const rawLine of block.split("\n")) {
|
|
1092
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
1093
|
+
if (!line || line.startsWith("[")) continue;
|
|
1094
|
+
const eq = line.indexOf("=");
|
|
1095
|
+
if (eq < 0) continue;
|
|
1096
|
+
const alias = line.slice(0, eq).trim();
|
|
1097
|
+
const value = line.slice(eq + 1).trim();
|
|
1098
|
+
if (!alias) continue;
|
|
1099
|
+
const shorthand = value.match(/^"([^"]+)"$/);
|
|
1100
|
+
if (shorthand) {
|
|
1101
|
+
const parts = shorthand[1].split(":");
|
|
1102
|
+
if (parts.length >= 2 && parts[0] && parts[1]) {
|
|
1103
|
+
catalog.set(alias, `${parts[0]}:${parts[1]}`);
|
|
1104
|
+
}
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
const moduleMatch = value.match(/module\s*=\s*"([^"]+)"/);
|
|
1108
|
+
if (moduleMatch) {
|
|
1109
|
+
const parts = moduleMatch[1].split(":");
|
|
1110
|
+
if (parts.length >= 2 && parts[0] && parts[1]) {
|
|
1111
|
+
catalog.set(alias, `${parts[0]}:${parts[1]}`);
|
|
1112
|
+
}
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
const groupMatch = value.match(/group\s*=\s*"([^"]+)"/);
|
|
1116
|
+
const nameMatch = value.match(/name\s*=\s*"([^"]+)"/);
|
|
1117
|
+
if (groupMatch && nameMatch) {
|
|
1118
|
+
catalog.set(alias, `${groupMatch[1]}:${nameMatch[1]}`);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
804
1122
|
function scanSDKs(rootDir, techStack) {
|
|
805
1123
|
const detectedSDKs = [];
|
|
806
1124
|
const dataCollected = /* @__PURE__ */ new Set();
|
|
@@ -852,7 +1170,7 @@ function getDependencies(rootDir, techStack) {
|
|
|
852
1170
|
const deps = [];
|
|
853
1171
|
switch (techStack) {
|
|
854
1172
|
case "flutter": {
|
|
855
|
-
const pubspecContent =
|
|
1173
|
+
const pubspecContent = readFileSafe2(join4(rootDir, "pubspec.yaml"));
|
|
856
1174
|
if (pubspecContent) {
|
|
857
1175
|
try {
|
|
858
1176
|
const pubspec = YAML2.parse(pubspecContent);
|
|
@@ -867,9 +1185,10 @@ function getDependencies(rootDir, techStack) {
|
|
|
867
1185
|
}
|
|
868
1186
|
break;
|
|
869
1187
|
}
|
|
1188
|
+
case "expo":
|
|
870
1189
|
case "react-native":
|
|
871
1190
|
case "capacitor": {
|
|
872
|
-
const pkgContent =
|
|
1191
|
+
const pkgContent = readFileSafe2(join4(rootDir, "package.json"));
|
|
873
1192
|
if (pkgContent) {
|
|
874
1193
|
try {
|
|
875
1194
|
const pkg = JSON.parse(pkgContent);
|
|
@@ -880,15 +1199,39 @@ function getDependencies(rootDir, techStack) {
|
|
|
880
1199
|
}
|
|
881
1200
|
break;
|
|
882
1201
|
}
|
|
1202
|
+
case "dotnet-maui": {
|
|
1203
|
+
const csprojFiles = [];
|
|
1204
|
+
try {
|
|
1205
|
+
for (const entry of readdirSync4(rootDir)) {
|
|
1206
|
+
if (entry.endsWith(".csproj")) csprojFiles.push(join4(rootDir, entry));
|
|
1207
|
+
}
|
|
1208
|
+
} catch {
|
|
1209
|
+
}
|
|
1210
|
+
const nested = findFiles(rootDir, [".csproj"], 3);
|
|
1211
|
+
for (const f of nested) {
|
|
1212
|
+
if (!csprojFiles.includes(f)) csprojFiles.push(f);
|
|
1213
|
+
}
|
|
1214
|
+
for (const path2 of csprojFiles) {
|
|
1215
|
+
const content = readFileSafe2(path2);
|
|
1216
|
+
if (!content) continue;
|
|
1217
|
+
const pkgMatches = content.matchAll(
|
|
1218
|
+
/<PackageReference\s+[^>]*Include\s*=\s*"([^"]+)"/g
|
|
1219
|
+
);
|
|
1220
|
+
for (const match of pkgMatches) {
|
|
1221
|
+
if (!deps.includes(match[1])) deps.push(match[1]);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
883
1226
|
case "swift": {
|
|
884
|
-
const podfileContent =
|
|
1227
|
+
const podfileContent = readFileSafe2(join4(rootDir, "Podfile"));
|
|
885
1228
|
if (podfileContent) {
|
|
886
1229
|
const podMatches = podfileContent.matchAll(/pod\s+['"]([^'"]+)['"]/g);
|
|
887
1230
|
for (const match of podMatches) {
|
|
888
1231
|
deps.push(match[1]);
|
|
889
1232
|
}
|
|
890
1233
|
}
|
|
891
|
-
const packageSwift =
|
|
1234
|
+
const packageSwift = readFileSafe2(join4(rootDir, "Package.swift"));
|
|
892
1235
|
if (packageSwift) {
|
|
893
1236
|
const urlMatches = packageSwift.matchAll(
|
|
894
1237
|
/\.package\([^)]*url:\s*"[^"]*\/([^/"]+?)(?:\.git)?"/g
|
|
@@ -899,7 +1242,7 @@ function getDependencies(rootDir, techStack) {
|
|
|
899
1242
|
}
|
|
900
1243
|
const pbxprojPath = findFile(rootDir, "project.pbxproj", 5);
|
|
901
1244
|
if (pbxprojPath) {
|
|
902
|
-
const pbxContent =
|
|
1245
|
+
const pbxContent = readFileSafe2(pbxprojPath);
|
|
903
1246
|
if (pbxContent) {
|
|
904
1247
|
const spmMatches = pbxContent.matchAll(
|
|
905
1248
|
/repositoryURL\s*=\s*"[^"]*\/([^/"]+?)(?:\.git)?"/g
|
|
@@ -914,7 +1257,7 @@ function getDependencies(rootDir, techStack) {
|
|
|
914
1257
|
const swiftFiles = findFiles(rootDir, [".swift"], 6);
|
|
915
1258
|
const nativeImports = /* @__PURE__ */ new Set();
|
|
916
1259
|
for (const file of swiftFiles.slice(0, 100)) {
|
|
917
|
-
const content =
|
|
1260
|
+
const content = readFileSafe2(file);
|
|
918
1261
|
if (!content) continue;
|
|
919
1262
|
const importMatches = content.matchAll(/^import\s+(\w+)/gm);
|
|
920
1263
|
for (const match of importMatches) {
|
|
@@ -927,29 +1270,51 @@ function getDependencies(rootDir, techStack) {
|
|
|
927
1270
|
deps.push(...nativeImports);
|
|
928
1271
|
break;
|
|
929
1272
|
}
|
|
930
|
-
case "kotlin":
|
|
931
|
-
|
|
932
|
-
const
|
|
1273
|
+
case "kotlin": {
|
|
1274
|
+
const catalog = /* @__PURE__ */ new Map();
|
|
1275
|
+
for (const p of [
|
|
1276
|
+
join4(rootDir, "gradle/libs.versions.toml"),
|
|
1277
|
+
join4(rootDir, "libs.versions.toml")
|
|
1278
|
+
]) {
|
|
1279
|
+
const content = readFileSafe2(p);
|
|
1280
|
+
if (content) parseVersionCatalog(content, catalog);
|
|
1281
|
+
}
|
|
1282
|
+
const gradleFiles = /* @__PURE__ */ new Set([
|
|
933
1283
|
join4(rootDir, "app/build.gradle"),
|
|
934
1284
|
join4(rootDir, "app/build.gradle.kts"),
|
|
935
1285
|
join4(rootDir, "build.gradle"),
|
|
936
1286
|
join4(rootDir, "build.gradle.kts")
|
|
937
|
-
];
|
|
938
|
-
for (const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1287
|
+
]);
|
|
1288
|
+
for (const f of findFiles(rootDir, ["build.gradle", "build.gradle.kts"], 5)) {
|
|
1289
|
+
gradleFiles.add(f);
|
|
1290
|
+
}
|
|
1291
|
+
const addCoord = (coord) => {
|
|
1292
|
+
const parts = coord.split(":");
|
|
1293
|
+
if (parts.length < 2) return;
|
|
1294
|
+
const [group, artifact] = parts;
|
|
1295
|
+
if (!group || !artifact) return;
|
|
1296
|
+
if (!deps.includes(artifact)) deps.push(artifact);
|
|
1297
|
+
const gav = `${group}:${artifact}`;
|
|
1298
|
+
if (!deps.includes(gav)) deps.push(gav);
|
|
1299
|
+
};
|
|
1300
|
+
const coordRegex = /['"]([a-zA-Z][\w.-]*\.[a-zA-Z][\w.-]*:[a-zA-Z][\w.-]*(?::[^'"\s]+)?)['"]/g;
|
|
1301
|
+
const libsRefRegex = /\blibs\.([a-zA-Z][\w.]*)/g;
|
|
1302
|
+
for (const path2 of gradleFiles) {
|
|
1303
|
+
const content = readFileSafe2(path2);
|
|
1304
|
+
if (!content) continue;
|
|
1305
|
+
for (const m of content.matchAll(coordRegex)) {
|
|
1306
|
+
addCoord(m[1]);
|
|
1307
|
+
}
|
|
1308
|
+
for (const m of content.matchAll(libsRefRegex)) {
|
|
1309
|
+
const ref = m[1];
|
|
1310
|
+
const resolved = catalog.get(ref) || catalog.get(ref.replace(/\./g, "-"));
|
|
1311
|
+
if (resolved) addCoord(resolved);
|
|
950
1312
|
}
|
|
951
1313
|
}
|
|
952
|
-
const androidFiles =
|
|
1314
|
+
const androidFiles = [
|
|
1315
|
+
...findFiles(join4(rootDir, "app/src"), [".kt", ".java"], 8),
|
|
1316
|
+
...findFiles(rootDir, [".kt", ".java"], 6)
|
|
1317
|
+
];
|
|
953
1318
|
const androidNativeImports = /* @__PURE__ */ new Set();
|
|
954
1319
|
const androidFrameworkPrefixes = [
|
|
955
1320
|
"com.google.android.gms.ads",
|
|
@@ -959,16 +1324,20 @@ function getDependencies(rootDir, techStack) {
|
|
|
959
1324
|
"com.android.billingclient",
|
|
960
1325
|
"androidx.health",
|
|
961
1326
|
"androidx.biometric",
|
|
962
|
-
"androidx.camera"
|
|
1327
|
+
"androidx.camera",
|
|
1328
|
+
"androidx.compose",
|
|
1329
|
+
"androidx.navigation.compose"
|
|
963
1330
|
];
|
|
964
|
-
|
|
965
|
-
|
|
1331
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1332
|
+
for (const file of androidFiles) {
|
|
1333
|
+
if (seen.has(file)) continue;
|
|
1334
|
+
seen.add(file);
|
|
1335
|
+
if (seen.size > 200) break;
|
|
1336
|
+
const content = readFileSafe2(file);
|
|
966
1337
|
if (!content) continue;
|
|
967
1338
|
for (const prefix of androidFrameworkPrefixes) {
|
|
968
|
-
if (content.includes(`import ${prefix}`)) {
|
|
969
|
-
|
|
970
|
-
androidNativeImports.add(prefix);
|
|
971
|
-
}
|
|
1339
|
+
if (content.includes(`import ${prefix}`) && !androidNativeImports.has(prefix)) {
|
|
1340
|
+
androidNativeImports.add(prefix);
|
|
972
1341
|
}
|
|
973
1342
|
}
|
|
974
1343
|
}
|
|
@@ -980,7 +1349,7 @@ function getDependencies(rootDir, techStack) {
|
|
|
980
1349
|
}
|
|
981
1350
|
|
|
982
1351
|
// src/analyzers/branding.ts
|
|
983
|
-
import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync as
|
|
1352
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
|
|
984
1353
|
import { join as join5, extname } from "path";
|
|
985
1354
|
function extractBranding(rootDir, techStack) {
|
|
986
1355
|
const result = {
|
|
@@ -1003,18 +1372,127 @@ function extractColors(rootDir, techStack) {
|
|
|
1003
1372
|
switch (techStack) {
|
|
1004
1373
|
case "flutter":
|
|
1005
1374
|
return extractFlutterColors(rootDir);
|
|
1375
|
+
case "expo":
|
|
1376
|
+
return extractExpoColors(rootDir);
|
|
1006
1377
|
case "react-native":
|
|
1007
1378
|
case "capacitor":
|
|
1008
1379
|
return extractJSColors(rootDir);
|
|
1009
1380
|
case "swift":
|
|
1010
1381
|
return extractSwiftColors(rootDir);
|
|
1011
1382
|
case "kotlin":
|
|
1012
|
-
case "java":
|
|
1013
1383
|
return extractAndroidColors(rootDir);
|
|
1384
|
+
case "dotnet-maui":
|
|
1385
|
+
return extractMauiColors(rootDir);
|
|
1014
1386
|
default:
|
|
1015
1387
|
return { primary: null, secondary: null, allColors: [] };
|
|
1016
1388
|
}
|
|
1017
1389
|
}
|
|
1390
|
+
function extractExpoColors(rootDir) {
|
|
1391
|
+
const hexColors = [];
|
|
1392
|
+
let primary = null;
|
|
1393
|
+
let secondary = null;
|
|
1394
|
+
const HEX = /^#[0-9a-fA-F]{6}$/;
|
|
1395
|
+
const normalize = (raw) => {
|
|
1396
|
+
if (!raw) return null;
|
|
1397
|
+
const v = raw.trim();
|
|
1398
|
+
if (HEX.test(v)) return v.toLowerCase();
|
|
1399
|
+
if (/^#[0-9a-fA-F]{8}$/.test(v)) return `#${v.slice(3).toLowerCase()}`;
|
|
1400
|
+
return null;
|
|
1401
|
+
};
|
|
1402
|
+
const appJsonContent = readFileSafe2(join5(rootDir, "app.json"));
|
|
1403
|
+
if (appJsonContent) {
|
|
1404
|
+
try {
|
|
1405
|
+
const data = JSON.parse(appJsonContent);
|
|
1406
|
+
const expo = data.expo || data;
|
|
1407
|
+
const candidates = [
|
|
1408
|
+
expo.primaryColor,
|
|
1409
|
+
expo.splash?.backgroundColor,
|
|
1410
|
+
expo.android?.adaptiveIcon?.backgroundColor,
|
|
1411
|
+
expo.android?.splash?.backgroundColor,
|
|
1412
|
+
expo.ios?.splash?.backgroundColor
|
|
1413
|
+
];
|
|
1414
|
+
for (const c of candidates) {
|
|
1415
|
+
const n = normalize(c || "");
|
|
1416
|
+
if (n) hexColors.push(n);
|
|
1417
|
+
}
|
|
1418
|
+
primary = normalize(expo.primaryColor || "") || null;
|
|
1419
|
+
} catch {
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
const jsResult = extractJSColors(rootDir);
|
|
1423
|
+
if (!primary) primary = jsResult.primary;
|
|
1424
|
+
if (!secondary) secondary = jsResult.secondary;
|
|
1425
|
+
hexColors.push(...jsResult.allColors);
|
|
1426
|
+
return {
|
|
1427
|
+
primary: primary || (hexColors[0] ?? null),
|
|
1428
|
+
secondary: secondary || (hexColors[1] ?? null),
|
|
1429
|
+
allColors: hexColors
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function extractMauiColors(rootDir) {
|
|
1433
|
+
const hexColors = [];
|
|
1434
|
+
const xamlPaths = [
|
|
1435
|
+
join5(rootDir, "Resources/Styles/Colors.xaml"),
|
|
1436
|
+
join5(rootDir, "Resources/Styles/Styles.xaml")
|
|
1437
|
+
];
|
|
1438
|
+
const allXaml = findFiles(rootDir, [".xaml"], 5);
|
|
1439
|
+
for (const p of allXaml) {
|
|
1440
|
+
if (/Colors\.xaml$/i.test(p) && !xamlPaths.includes(p)) xamlPaths.push(p);
|
|
1441
|
+
}
|
|
1442
|
+
for (const path2 of xamlPaths) {
|
|
1443
|
+
const content = readFileSafe2(path2);
|
|
1444
|
+
if (!content) continue;
|
|
1445
|
+
const colorMatches = content.matchAll(
|
|
1446
|
+
/<Color\s+x:Key="([^"]+)"\s*>\s*#([0-9a-fA-F]{6,8})\s*<\/Color>/g
|
|
1447
|
+
);
|
|
1448
|
+
for (const match of colorMatches) {
|
|
1449
|
+
const name = match[1].toLowerCase();
|
|
1450
|
+
let hex = match[2];
|
|
1451
|
+
if (hex.length === 8) hex = hex.substring(2);
|
|
1452
|
+
const color = `#${hex.toLowerCase()}`;
|
|
1453
|
+
if (name.includes("primary") && !name.includes("dark") && !name.includes("light")) {
|
|
1454
|
+
hexColors.unshift(color);
|
|
1455
|
+
} else {
|
|
1456
|
+
hexColors.push(color);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
const brushMatches = content.matchAll(
|
|
1460
|
+
/<SolidColorBrush\s+[^>]*Color\s*=\s*"#([0-9a-fA-F]{6,8})"/g
|
|
1461
|
+
);
|
|
1462
|
+
for (const match of brushMatches) {
|
|
1463
|
+
let hex = match[1];
|
|
1464
|
+
if (hex.length === 8) hex = hex.substring(2);
|
|
1465
|
+
hexColors.push(`#${hex.toLowerCase()}`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
const csFiles = findFiles(rootDir, [".cs"], 5);
|
|
1469
|
+
for (const file of csFiles.slice(0, 50)) {
|
|
1470
|
+
const content = readFileSafe2(file);
|
|
1471
|
+
if (!content) continue;
|
|
1472
|
+
const argbMatches = content.matchAll(
|
|
1473
|
+
/Color\.FromArgb\s*\(\s*"#?([0-9a-fA-F]{6,8})"/g
|
|
1474
|
+
);
|
|
1475
|
+
for (const match of argbMatches) {
|
|
1476
|
+
let hex = match[1];
|
|
1477
|
+
if (hex.length === 8) hex = hex.substring(2);
|
|
1478
|
+
hexColors.push(`#${hex.toLowerCase()}`);
|
|
1479
|
+
}
|
|
1480
|
+
const rgbMatches = content.matchAll(
|
|
1481
|
+
/Color\.FromRgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g
|
|
1482
|
+
);
|
|
1483
|
+
for (const match of rgbMatches) {
|
|
1484
|
+
const r = parseInt(match[1]).toString(16).padStart(2, "0");
|
|
1485
|
+
const g = parseInt(match[2]).toString(16).padStart(2, "0");
|
|
1486
|
+
const b = parseInt(match[3]).toString(16).padStart(2, "0");
|
|
1487
|
+
hexColors.push(`#${r}${g}${b}`);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return {
|
|
1491
|
+
primary: hexColors[0] ?? null,
|
|
1492
|
+
secondary: hexColors[1] ?? null,
|
|
1493
|
+
allColors: hexColors
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1018
1496
|
function isIrrelevantColor(hex) {
|
|
1019
1497
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
1020
1498
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
@@ -1039,7 +1517,7 @@ function extractFlutterColors(rootDir) {
|
|
|
1039
1517
|
const dartFiles = findFiles(rootDir, [".dart"], 6);
|
|
1040
1518
|
const hexColors = [];
|
|
1041
1519
|
for (const file of dartFiles.slice(0, 50)) {
|
|
1042
|
-
const content =
|
|
1520
|
+
const content = readFileSafe2(file);
|
|
1043
1521
|
if (!content) continue;
|
|
1044
1522
|
const colorMatches = content.matchAll(/Color\(0[xX]([0-9a-fA-F]{8})\)/g);
|
|
1045
1523
|
for (const match of colorMatches) {
|
|
@@ -1091,7 +1569,7 @@ function extractJSColors(rootDir) {
|
|
|
1091
1569
|
(f) => f.endsWith(fileName)
|
|
1092
1570
|
);
|
|
1093
1571
|
for (const file of files) {
|
|
1094
|
-
const content =
|
|
1572
|
+
const content = readFileSafe2(file);
|
|
1095
1573
|
if (!content) continue;
|
|
1096
1574
|
const matches = content.matchAll(
|
|
1097
1575
|
/['"]#([0-9a-fA-F]{6})['"]/g
|
|
@@ -1132,7 +1610,7 @@ function extractSwiftColors(rootDir) {
|
|
|
1132
1610
|
}
|
|
1133
1611
|
for (const catalog of assetsCatalogs) {
|
|
1134
1612
|
try {
|
|
1135
|
-
const entries =
|
|
1613
|
+
const entries = readdirSync5(catalog);
|
|
1136
1614
|
for (const entry of entries) {
|
|
1137
1615
|
if (entry.endsWith(".colorset") && entry !== "AccentColor.colorset") {
|
|
1138
1616
|
const color = parseColorSet(join5(catalog, entry));
|
|
@@ -1145,7 +1623,7 @@ function extractSwiftColors(rootDir) {
|
|
|
1145
1623
|
}
|
|
1146
1624
|
const swiftFiles = findFiles(rootDir, [".swift"], 6);
|
|
1147
1625
|
for (const file of swiftFiles.slice(0, 50)) {
|
|
1148
|
-
const content =
|
|
1626
|
+
const content = readFileSafe2(file);
|
|
1149
1627
|
if (!content) continue;
|
|
1150
1628
|
const hexMatches = content.matchAll(
|
|
1151
1629
|
/(?:UIColor|Color)\s*\(\s*(?:hex|hexString)\s*:\s*(?:0[xX]|"#?)([0-9a-fA-F]{6})/g
|
|
@@ -1181,51 +1659,103 @@ function extractSwiftColors(rootDir) {
|
|
|
1181
1659
|
}
|
|
1182
1660
|
function extractAndroidColors(rootDir) {
|
|
1183
1661
|
const hexColors = [];
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1662
|
+
let primary = null;
|
|
1663
|
+
let secondary = null;
|
|
1664
|
+
const normalize = (hexDigits) => {
|
|
1665
|
+
const h = hexDigits.length === 8 ? hexDigits.slice(2) : hexDigits;
|
|
1666
|
+
return `#${h.toLowerCase()}`;
|
|
1667
|
+
};
|
|
1668
|
+
const colorMap = /* @__PURE__ */ new Map();
|
|
1669
|
+
const colorsFiles = findAllFiles(rootDir, "colors.xml", 8);
|
|
1670
|
+
for (const path2 of colorsFiles) {
|
|
1671
|
+
const content = readFileSafe2(path2);
|
|
1672
|
+
if (!content) continue;
|
|
1673
|
+
const matches = content.matchAll(
|
|
1674
|
+
/<color\s+name="([^"]+)"[^>]*>\s*#([0-9a-fA-F]{6,8})\s*<\/color>/g
|
|
1675
|
+
);
|
|
1676
|
+
for (const m of matches) {
|
|
1677
|
+
const hex = normalize(m[2]);
|
|
1678
|
+
colorMap.set(m[1], hex);
|
|
1679
|
+
hexColors.push(hex);
|
|
1680
|
+
const name = m[1].toLowerCase();
|
|
1681
|
+
if (name.includes("primary") && !name.includes("variant") && !name.includes("dark") && !name.includes("container") && !primary) {
|
|
1682
|
+
primary = hex;
|
|
1683
|
+
} else if ((name.includes("secondary") || name.includes("accent")) && !secondary) {
|
|
1684
|
+
secondary = hex;
|
|
1205
1685
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
const themeFiles = [
|
|
1689
|
+
...findAllFiles(rootDir, "themes.xml", 8),
|
|
1690
|
+
...findAllFiles(rootDir, "styles.xml", 8)
|
|
1691
|
+
];
|
|
1692
|
+
for (const path2 of themeFiles) {
|
|
1693
|
+
const content = readFileSafe2(path2);
|
|
1694
|
+
if (!content) continue;
|
|
1695
|
+
const itemMatches = content.matchAll(
|
|
1696
|
+
/<item\s+name="(?:android:)?([A-Za-z]+)"[^>]*>\s*(?:@color\/([A-Za-z0-9_]+)|#([0-9a-fA-F]{6,8}))\s*<\/item>/g
|
|
1697
|
+
);
|
|
1698
|
+
for (const m of itemMatches) {
|
|
1699
|
+
const itemName = m[1].toLowerCase();
|
|
1700
|
+
if (!itemName.startsWith("color")) continue;
|
|
1701
|
+
let hex = null;
|
|
1702
|
+
if (m[2]) hex = colorMap.get(m[2]) || null;
|
|
1703
|
+
else if (m[3]) hex = normalize(m[3]);
|
|
1704
|
+
if (!hex) continue;
|
|
1705
|
+
if (itemName === "colorprimary" || itemName === "colorprimarydark") {
|
|
1706
|
+
if (!primary) primary = hex;
|
|
1707
|
+
} else if (itemName === "colorsecondary" || itemName === "colortertiary" || itemName === "coloraccent") {
|
|
1708
|
+
if (!secondary) secondary = hex;
|
|
1212
1709
|
}
|
|
1710
|
+
hexColors.push(hex);
|
|
1213
1711
|
}
|
|
1214
1712
|
}
|
|
1215
1713
|
const ktFiles = findFiles(rootDir, [".kt"], 6);
|
|
1216
|
-
for (const file of ktFiles.slice(0,
|
|
1714
|
+
for (const file of ktFiles.slice(0, 80)) {
|
|
1217
1715
|
const fileName = file.toLowerCase();
|
|
1218
1716
|
if (!fileName.includes("color") && !fileName.includes("theme")) continue;
|
|
1219
|
-
const content =
|
|
1717
|
+
const content = readFileSafe2(file);
|
|
1220
1718
|
if (!content) continue;
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1719
|
+
const valMap = /* @__PURE__ */ new Map();
|
|
1720
|
+
const valMatches = content.matchAll(
|
|
1721
|
+
/val\s+(\w+)\s*(?::\s*Color\s*)?=\s*Color\s*\(\s*0[xX]([0-9a-fA-F]{8})\s*\)/g
|
|
1722
|
+
);
|
|
1723
|
+
for (const m of valMatches) {
|
|
1724
|
+
const hex = normalize(m[2]);
|
|
1725
|
+
valMap.set(m[1], hex);
|
|
1726
|
+
hexColors.push(hex);
|
|
1727
|
+
}
|
|
1728
|
+
const literalMatches = content.matchAll(/Color\s*\(\s*0[xX]([0-9a-fA-F]{8})\s*\)/g);
|
|
1729
|
+
for (const m of literalMatches) {
|
|
1730
|
+
hexColors.push(normalize(m[1]));
|
|
1731
|
+
}
|
|
1732
|
+
const schemeMatch = content.match(
|
|
1733
|
+
/(?:light|dark)ColorScheme\s*\(\s*([\s\S]*?)\n\s*\)/
|
|
1734
|
+
);
|
|
1735
|
+
if (schemeMatch) {
|
|
1736
|
+
const body = schemeMatch[1];
|
|
1737
|
+
const resolve = (key) => {
|
|
1738
|
+
const re = new RegExp(
|
|
1739
|
+
`\\b${key}\\s*=\\s*(?:Color\\s*\\(\\s*0[xX]([0-9a-fA-F]{8})\\s*\\)|(\\w+))`
|
|
1740
|
+
);
|
|
1741
|
+
const m = body.match(re);
|
|
1742
|
+
if (!m) return null;
|
|
1743
|
+
if (m[1]) return normalize(m[1]);
|
|
1744
|
+
return valMap.get(m[2]) || null;
|
|
1745
|
+
};
|
|
1746
|
+
if (!primary) {
|
|
1747
|
+
const p = resolve("primary");
|
|
1748
|
+
if (p) primary = p;
|
|
1749
|
+
}
|
|
1750
|
+
if (!secondary) {
|
|
1751
|
+
const s = resolve("secondary") || resolve("tertiary");
|
|
1752
|
+
if (s) secondary = s;
|
|
1753
|
+
}
|
|
1224
1754
|
}
|
|
1225
1755
|
}
|
|
1226
1756
|
return {
|
|
1227
|
-
primary:
|
|
1228
|
-
secondary:
|
|
1757
|
+
primary: primary || hexColors[0] || null,
|
|
1758
|
+
secondary: secondary || hexColors[1] || null,
|
|
1229
1759
|
allColors: hexColors
|
|
1230
1760
|
};
|
|
1231
1761
|
}
|
|
@@ -1234,7 +1764,7 @@ function findAssetsCatalogs(rootDir) {
|
|
|
1234
1764
|
function search(dir, depth) {
|
|
1235
1765
|
if (depth > 5) return;
|
|
1236
1766
|
try {
|
|
1237
|
-
const entries =
|
|
1767
|
+
const entries = readdirSync5(dir);
|
|
1238
1768
|
for (const entry of entries) {
|
|
1239
1769
|
if (entry.startsWith(".") || entry === "Pods" || entry === "DerivedData") continue;
|
|
1240
1770
|
const fullPath = join5(dir, entry);
|
|
@@ -1260,7 +1790,7 @@ function parseColorSetInCatalog(catalogPath, colorName) {
|
|
|
1260
1790
|
}
|
|
1261
1791
|
function parseColorSet(colorSetPath) {
|
|
1262
1792
|
const contentsPath = join5(colorSetPath, "Contents.json");
|
|
1263
|
-
const content =
|
|
1793
|
+
const content = readFileSafe2(contentsPath);
|
|
1264
1794
|
if (!content) return null;
|
|
1265
1795
|
try {
|
|
1266
1796
|
const data = JSON.parse(content);
|
|
@@ -1300,6 +1830,18 @@ function findAppIcon(rootDir, techStack) {
|
|
|
1300
1830
|
join5(rootDir, "android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png")
|
|
1301
1831
|
);
|
|
1302
1832
|
break;
|
|
1833
|
+
case "expo":
|
|
1834
|
+
iconPaths.push(
|
|
1835
|
+
...resolveExpoIconPaths(rootDir),
|
|
1836
|
+
// Common Expo defaults
|
|
1837
|
+
join5(rootDir, "assets/icon.png"),
|
|
1838
|
+
join5(rootDir, "assets/adaptive-icon.png"),
|
|
1839
|
+
join5(rootDir, "assets/images/icon.png"),
|
|
1840
|
+
join5(rootDir, "assets/images/adaptive-icon.png"),
|
|
1841
|
+
...findIconsInAppIconSet(join5(rootDir, "ios")),
|
|
1842
|
+
join5(rootDir, "android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png")
|
|
1843
|
+
);
|
|
1844
|
+
break;
|
|
1303
1845
|
case "react-native":
|
|
1304
1846
|
case "capacitor":
|
|
1305
1847
|
iconPaths.push(
|
|
@@ -1315,13 +1857,47 @@ function findAppIcon(rootDir, techStack) {
|
|
|
1315
1857
|
...findIconsInAppIconSet(rootDir)
|
|
1316
1858
|
);
|
|
1317
1859
|
break;
|
|
1318
|
-
case "kotlin":
|
|
1319
|
-
|
|
1860
|
+
case "kotlin": {
|
|
1861
|
+
const modules = ["app", "mobile", "androidApp", "presentation", ""];
|
|
1862
|
+
const densities = ["xxxhdpi", "xxhdpi", "xhdpi"];
|
|
1863
|
+
for (const mod of modules) {
|
|
1864
|
+
const prefix = mod ? `${mod}/` : "";
|
|
1865
|
+
iconPaths.push(
|
|
1866
|
+
join5(rootDir, `${prefix}src/main/ic_launcher-playstore.png`),
|
|
1867
|
+
join5(rootDir, `${prefix}ic_launcher-playstore.png`)
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
for (const mod of modules) {
|
|
1871
|
+
const prefix = mod ? `${mod}/` : "";
|
|
1872
|
+
for (const d of densities) {
|
|
1873
|
+
iconPaths.push(
|
|
1874
|
+
join5(rootDir, `${prefix}src/main/res/mipmap-${d}/ic_launcher_foreground.png`),
|
|
1875
|
+
join5(rootDir, `${prefix}src/main/res/mipmap-${d}/ic_launcher_foreground.webp`)
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
for (const mod of modules) {
|
|
1880
|
+
const prefix = mod ? `${mod}/` : "";
|
|
1881
|
+
for (const d of densities) {
|
|
1882
|
+
iconPaths.push(
|
|
1883
|
+
join5(rootDir, `${prefix}src/main/res/mipmap-${d}/ic_launcher.png`),
|
|
1884
|
+
join5(rootDir, `${prefix}src/main/res/mipmap-${d}/ic_launcher.webp`),
|
|
1885
|
+
join5(rootDir, `${prefix}src/main/res/mipmap-${d}/ic_launcher_round.png`),
|
|
1886
|
+
join5(rootDir, `${prefix}src/main/res/mipmap-${d}/ic_launcher_round.webp`)
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
break;
|
|
1891
|
+
}
|
|
1892
|
+
case "dotnet-maui":
|
|
1320
1893
|
iconPaths.push(
|
|
1321
|
-
join5(rootDir, "
|
|
1322
|
-
join5(rootDir, "
|
|
1323
|
-
join5(rootDir, "
|
|
1324
|
-
join5(rootDir, "
|
|
1894
|
+
join5(rootDir, "Resources/AppIcon/appicon.png"),
|
|
1895
|
+
join5(rootDir, "Resources/AppIcon/appiconfg.png"),
|
|
1896
|
+
join5(rootDir, "Resources/Images/icon.png"),
|
|
1897
|
+
join5(rootDir, "Resources/Images/appicon.png"),
|
|
1898
|
+
// SVG fallback (MAUI default is SVG appicon.svg)
|
|
1899
|
+
join5(rootDir, "Resources/AppIcon/appicon.svg"),
|
|
1900
|
+
join5(rootDir, "Resources/AppIcon/appiconfg.svg")
|
|
1325
1901
|
);
|
|
1326
1902
|
break;
|
|
1327
1903
|
}
|
|
@@ -1333,7 +1909,7 @@ function findAppIcon(rootDir, techStack) {
|
|
|
1333
1909
|
if (stat.size < 100) continue;
|
|
1334
1910
|
const buffer = readFileSync2(iconPath);
|
|
1335
1911
|
const ext = extname(iconPath).toLowerCase();
|
|
1336
|
-
const mime = ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
|
|
1912
|
+
const mime = ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : ext === ".svg" ? "image/svg+xml" : ext === ".webp" ? "image/webp" : "image/png";
|
|
1337
1913
|
const base64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
|
1338
1914
|
return { base64, path: iconPath };
|
|
1339
1915
|
} catch {
|
|
@@ -1343,17 +1919,38 @@ function findAppIcon(rootDir, techStack) {
|
|
|
1343
1919
|
}
|
|
1344
1920
|
return { base64: null, path: null };
|
|
1345
1921
|
}
|
|
1922
|
+
function resolveExpoIconPaths(rootDir) {
|
|
1923
|
+
const paths = [];
|
|
1924
|
+
const appJsonContent = readFileSafe2(join5(rootDir, "app.json"));
|
|
1925
|
+
if (!appJsonContent) return paths;
|
|
1926
|
+
try {
|
|
1927
|
+
const data = JSON.parse(appJsonContent);
|
|
1928
|
+
const expo = data.expo || data;
|
|
1929
|
+
const candidates = [
|
|
1930
|
+
expo.icon,
|
|
1931
|
+
expo.android?.icon,
|
|
1932
|
+
expo.android?.adaptiveIcon?.foregroundImage,
|
|
1933
|
+
expo.ios?.icon
|
|
1934
|
+
].filter((v) => typeof v === "string");
|
|
1935
|
+
for (const rel of candidates) {
|
|
1936
|
+
const cleaned = rel.replace(/^\.?\//, "");
|
|
1937
|
+
paths.push(join5(rootDir, cleaned));
|
|
1938
|
+
}
|
|
1939
|
+
} catch {
|
|
1940
|
+
}
|
|
1941
|
+
return paths;
|
|
1942
|
+
}
|
|
1346
1943
|
function findIconsInAppIconSet(searchDir) {
|
|
1347
1944
|
const icons = [];
|
|
1348
1945
|
function searchRecursive(dir, depth) {
|
|
1349
1946
|
if (depth > 6) return;
|
|
1350
1947
|
try {
|
|
1351
|
-
const entries =
|
|
1948
|
+
const entries = readdirSync5(dir);
|
|
1352
1949
|
for (const entry of entries) {
|
|
1353
1950
|
const fullPath = join5(dir, entry);
|
|
1354
1951
|
if (entry === "AppIcon.appiconset") {
|
|
1355
1952
|
const contentsPath = join5(fullPath, "Contents.json");
|
|
1356
|
-
const content =
|
|
1953
|
+
const content = readFileSafe2(contentsPath);
|
|
1357
1954
|
if (content) {
|
|
1358
1955
|
try {
|
|
1359
1956
|
const data = JSON.parse(content);
|
|
@@ -1372,7 +1969,7 @@ function findIconsInAppIconSet(searchDir) {
|
|
|
1372
1969
|
}
|
|
1373
1970
|
}
|
|
1374
1971
|
try {
|
|
1375
|
-
const iconEntries =
|
|
1972
|
+
const iconEntries = readdirSync5(fullPath);
|
|
1376
1973
|
for (const ie of iconEntries) {
|
|
1377
1974
|
if (ie.endsWith(".png")) {
|
|
1378
1975
|
icons.push(join5(fullPath, ie));
|
|
@@ -1400,7 +1997,7 @@ function findIconsInAppIconSet(searchDir) {
|
|
|
1400
1997
|
}
|
|
1401
1998
|
|
|
1402
1999
|
// src/analyzers/font-detector.ts
|
|
1403
|
-
import { existsSync as existsSync4, readdirSync as
|
|
2000
|
+
import { existsSync as existsSync4, readdirSync as readdirSync6, statSync as statSync3 } from "fs";
|
|
1404
2001
|
import { join as join6, extname as extname2, basename } from "path";
|
|
1405
2002
|
import yaml from "yaml";
|
|
1406
2003
|
import plist2 from "plist";
|
|
@@ -1452,7 +2049,7 @@ function scanForFontFiles(dir, maxDepth = 3) {
|
|
|
1452
2049
|
function walk(d, depth) {
|
|
1453
2050
|
if (depth > maxDepth) return;
|
|
1454
2051
|
try {
|
|
1455
|
-
const entries =
|
|
2052
|
+
const entries = readdirSync6(d);
|
|
1456
2053
|
for (const entry of entries) {
|
|
1457
2054
|
if (entry.startsWith(".")) continue;
|
|
1458
2055
|
const fullPath = join6(d, entry);
|
|
@@ -1478,7 +2075,7 @@ function scanForFontFiles(dir, maxDepth = 3) {
|
|
|
1478
2075
|
function detectFlutterFonts(rootDir) {
|
|
1479
2076
|
const fonts = [];
|
|
1480
2077
|
const pubspecPath = join6(rootDir, "pubspec.yaml");
|
|
1481
|
-
const pubspecContent =
|
|
2078
|
+
const pubspecContent = readFileSafe2(pubspecPath);
|
|
1482
2079
|
if (pubspecContent) {
|
|
1483
2080
|
try {
|
|
1484
2081
|
const doc = yaml.parse(pubspecContent);
|
|
@@ -1495,7 +2092,7 @@ function detectFlutterFonts(rootDir) {
|
|
|
1495
2092
|
}
|
|
1496
2093
|
const dartFiles = findFiles(rootDir, [".dart"], 6);
|
|
1497
2094
|
for (const file of dartFiles.slice(0, 80)) {
|
|
1498
|
-
const content =
|
|
2095
|
+
const content = readFileSafe2(file);
|
|
1499
2096
|
if (!content) continue;
|
|
1500
2097
|
const fontFamilyMatches = content.matchAll(/fontFamily\s*:\s*['"]([^'"]+)['"]/g);
|
|
1501
2098
|
for (const match of fontFamilyMatches) {
|
|
@@ -1518,7 +2115,7 @@ function detectJSFonts(rootDir) {
|
|
|
1518
2115
|
const fonts = [];
|
|
1519
2116
|
const jsFiles = findFiles(rootDir, [".ts", ".tsx", ".js", ".jsx"], 5);
|
|
1520
2117
|
for (const file of jsFiles.slice(0, 100)) {
|
|
1521
|
-
const content =
|
|
2118
|
+
const content = readFileSafe2(file);
|
|
1522
2119
|
if (!content) continue;
|
|
1523
2120
|
const matches = content.matchAll(/fontFamily\s*:\s*['"]([^'"]+)['"]/g);
|
|
1524
2121
|
for (const match of matches) {
|
|
@@ -1542,7 +2139,7 @@ function detectSwiftFonts(rootDir) {
|
|
|
1542
2139
|
const fonts = [];
|
|
1543
2140
|
const swiftFiles = findFiles(rootDir, [".swift"], 6);
|
|
1544
2141
|
for (const file of swiftFiles.slice(0, 80)) {
|
|
1545
|
-
const content =
|
|
2142
|
+
const content = readFileSafe2(file);
|
|
1546
2143
|
if (!content) continue;
|
|
1547
2144
|
const uiFontMatches = content.matchAll(/UIFont\s*\(\s*name:\s*"([^"]+)"/g);
|
|
1548
2145
|
for (const match of uiFontMatches) {
|
|
@@ -1558,7 +2155,7 @@ function detectSwiftFonts(rootDir) {
|
|
|
1558
2155
|
...findPlistFiles(rootDir)
|
|
1559
2156
|
];
|
|
1560
2157
|
for (const plistPath of plistPaths) {
|
|
1561
|
-
const content =
|
|
2158
|
+
const content = readFileSafe2(plistPath);
|
|
1562
2159
|
if (!content) continue;
|
|
1563
2160
|
try {
|
|
1564
2161
|
const data = plist2.parse(content);
|
|
@@ -1590,7 +2187,7 @@ function findPlistFiles(rootDir) {
|
|
|
1590
2187
|
function search(dir, depth) {
|
|
1591
2188
|
if (depth > 4) return;
|
|
1592
2189
|
try {
|
|
1593
|
-
const entries =
|
|
2190
|
+
const entries = readdirSync6(dir);
|
|
1594
2191
|
for (const entry of entries) {
|
|
1595
2192
|
if (entry.startsWith(".") || entry === "Pods" || entry === "DerivedData" || entry === "build") continue;
|
|
1596
2193
|
const fullPath = join6(dir, entry);
|
|
@@ -1625,7 +2222,7 @@ function detectAndroidFonts(rootDir) {
|
|
|
1625
2222
|
}
|
|
1626
2223
|
const xmlFiles = findFiles(rootDir, [".xml"], 5);
|
|
1627
2224
|
for (const file of xmlFiles.slice(0, 50)) {
|
|
1628
|
-
const content =
|
|
2225
|
+
const content = readFileSafe2(file);
|
|
1629
2226
|
if (!content) continue;
|
|
1630
2227
|
const matches = content.matchAll(/android:fontFamily\s*=\s*"(?:@font\/)?([^"]+)"/g);
|
|
1631
2228
|
for (const match of matches) {
|
|
@@ -1635,7 +2232,7 @@ function detectAndroidFonts(rootDir) {
|
|
|
1635
2232
|
}
|
|
1636
2233
|
const ktFiles = findFiles(rootDir, [".kt"], 6);
|
|
1637
2234
|
for (const file of ktFiles.slice(0, 80)) {
|
|
1638
|
-
const content =
|
|
2235
|
+
const content = readFileSafe2(file);
|
|
1639
2236
|
if (!content) continue;
|
|
1640
2237
|
const fontResMatches = content.matchAll(/Font\s*\(\s*R\.font\.(\w+)/g);
|
|
1641
2238
|
for (const match of fontResMatches) {
|
|
@@ -1646,12 +2243,111 @@ function detectAndroidFonts(rootDir) {
|
|
|
1646
2243
|
}
|
|
1647
2244
|
return fonts;
|
|
1648
2245
|
}
|
|
2246
|
+
function detectExpoFonts(rootDir) {
|
|
2247
|
+
const fonts = [];
|
|
2248
|
+
const fontDirs = [
|
|
2249
|
+
join6(rootDir, "assets/fonts"),
|
|
2250
|
+
join6(rootDir, "src/assets/fonts"),
|
|
2251
|
+
join6(rootDir, "assets")
|
|
2252
|
+
];
|
|
2253
|
+
for (const dir of fontDirs) {
|
|
2254
|
+
const fontFiles = scanForFontFiles(dir, 2);
|
|
2255
|
+
for (const f of fontFiles) {
|
|
2256
|
+
fonts.push(fontNameFromFile(f));
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
const jsFiles = findFiles(rootDir, [".ts", ".tsx", ".js", ".jsx"], 6);
|
|
2260
|
+
for (const file of jsFiles.slice(0, 100)) {
|
|
2261
|
+
const content = readFileSafe2(file);
|
|
2262
|
+
if (!content) continue;
|
|
2263
|
+
const useFontsMatch = content.match(/useFonts\s*\(\s*\{([\s\S]*?)\}\s*\)/);
|
|
2264
|
+
if (useFontsMatch) {
|
|
2265
|
+
const block = useFontsMatch[1];
|
|
2266
|
+
const keyMatches = block.matchAll(/['"`]([^'"`]+)['"`]\s*:/g);
|
|
2267
|
+
for (const m of keyMatches) {
|
|
2268
|
+
fonts.push(m[1]);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
const loadAsyncMatch = content.match(/Font\.loadAsync\s*\(\s*\{([\s\S]*?)\}\s*\)/);
|
|
2272
|
+
if (loadAsyncMatch) {
|
|
2273
|
+
const block = loadAsyncMatch[1];
|
|
2274
|
+
const keyMatches = block.matchAll(/['"`]([^'"`]+)['"`]\s*:/g);
|
|
2275
|
+
for (const m of keyMatches) {
|
|
2276
|
+
fonts.push(m[1]);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
const familyMatches = content.matchAll(/fontFamily\s*:\s*['"]([^'"]+)['"]/g);
|
|
2280
|
+
for (const m of familyMatches) {
|
|
2281
|
+
fonts.push(m[1]);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
const appJsonContent = readFileSafe2(join6(rootDir, "app.json"));
|
|
2285
|
+
if (appJsonContent) {
|
|
2286
|
+
try {
|
|
2287
|
+
const data = JSON.parse(appJsonContent);
|
|
2288
|
+
const expoFonts = data.expo?.fonts || data.fonts;
|
|
2289
|
+
if (Array.isArray(expoFonts)) {
|
|
2290
|
+
for (const entry of expoFonts) {
|
|
2291
|
+
if (typeof entry === "string") {
|
|
2292
|
+
fonts.push(fontNameFromFile(entry));
|
|
2293
|
+
} else if (entry?.name) {
|
|
2294
|
+
fonts.push(entry.name);
|
|
2295
|
+
} else if (entry?.path) {
|
|
2296
|
+
fonts.push(fontNameFromFile(entry.path));
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
} catch {
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
return fonts;
|
|
2304
|
+
}
|
|
2305
|
+
function detectMauiFonts(rootDir) {
|
|
2306
|
+
const fonts = [];
|
|
2307
|
+
const fontDirs = [
|
|
2308
|
+
join6(rootDir, "Resources/Fonts"),
|
|
2309
|
+
join6(rootDir, "Resources/Font")
|
|
2310
|
+
];
|
|
2311
|
+
for (const dir of fontDirs) {
|
|
2312
|
+
const fontFiles = scanForFontFiles(dir, 2);
|
|
2313
|
+
for (const f of fontFiles) {
|
|
2314
|
+
fonts.push(fontNameFromFile(f));
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
const csFiles = findFiles(rootDir, [".cs"], 4);
|
|
2318
|
+
for (const file of csFiles.slice(0, 30)) {
|
|
2319
|
+
const content = readFileSafe2(file);
|
|
2320
|
+
if (!content) continue;
|
|
2321
|
+
if (!content.includes("AddFont")) continue;
|
|
2322
|
+
const matches = content.matchAll(
|
|
2323
|
+
/\.AddFont\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g
|
|
2324
|
+
);
|
|
2325
|
+
for (const match of matches) {
|
|
2326
|
+
fonts.push(match[2]);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
const xamlFiles = findFiles(rootDir, [".xaml"], 5);
|
|
2330
|
+
for (const file of xamlFiles.slice(0, 50)) {
|
|
2331
|
+
const content = readFileSafe2(file);
|
|
2332
|
+
if (!content) continue;
|
|
2333
|
+
const matches = content.matchAll(/FontFamily\s*=\s*"([^"]+)"/g);
|
|
2334
|
+
for (const match of matches) {
|
|
2335
|
+
const v = match[1];
|
|
2336
|
+
if (v.startsWith("{") || v.includes(",")) continue;
|
|
2337
|
+
fonts.push(v);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
return fonts;
|
|
2341
|
+
}
|
|
1649
2342
|
function detectFonts(rootDir, techStack) {
|
|
1650
2343
|
let rawFonts;
|
|
1651
2344
|
switch (techStack) {
|
|
1652
2345
|
case "flutter":
|
|
1653
2346
|
rawFonts = detectFlutterFonts(rootDir);
|
|
1654
2347
|
break;
|
|
2348
|
+
case "expo":
|
|
2349
|
+
rawFonts = detectExpoFonts(rootDir);
|
|
2350
|
+
break;
|
|
1655
2351
|
case "react-native":
|
|
1656
2352
|
case "capacitor":
|
|
1657
2353
|
rawFonts = detectJSFonts(rootDir);
|
|
@@ -1660,9 +2356,11 @@ function detectFonts(rootDir, techStack) {
|
|
|
1660
2356
|
rawFonts = detectSwiftFonts(rootDir);
|
|
1661
2357
|
break;
|
|
1662
2358
|
case "kotlin":
|
|
1663
|
-
case "java":
|
|
1664
2359
|
rawFonts = detectAndroidFonts(rootDir);
|
|
1665
2360
|
break;
|
|
2361
|
+
case "dotnet-maui":
|
|
2362
|
+
rawFonts = detectMauiFonts(rootDir);
|
|
2363
|
+
break;
|
|
1666
2364
|
default:
|
|
1667
2365
|
rawFonts = [];
|
|
1668
2366
|
}
|
|
@@ -1681,7 +2379,7 @@ function detectFonts(rootDir, techStack) {
|
|
|
1681
2379
|
}
|
|
1682
2380
|
|
|
1683
2381
|
// src/analyzers/source-reader.ts
|
|
1684
|
-
import { readdirSync as
|
|
2382
|
+
import { readdirSync as readdirSync7, statSync as statSync4 } from "fs";
|
|
1685
2383
|
import { join as join7, relative as relative2 } from "path";
|
|
1686
2384
|
var README_NAMES = [
|
|
1687
2385
|
"README.md",
|
|
@@ -1721,7 +2419,7 @@ var TREE_IGNORE = /* @__PURE__ */ new Set([
|
|
|
1721
2419
|
]);
|
|
1722
2420
|
function readReadme(rootDir) {
|
|
1723
2421
|
for (const name of README_NAMES) {
|
|
1724
|
-
const content =
|
|
2422
|
+
const content = readFileSafe2(join7(rootDir, name));
|
|
1725
2423
|
if (content && content.trim().length > 0) {
|
|
1726
2424
|
return content.substring(0, 1e4);
|
|
1727
2425
|
}
|
|
@@ -1741,7 +2439,7 @@ function readSourceCode(rootDir, techStack, maxTotalChars = 5e4) {
|
|
|
1741
2439
|
let totalChars = 0;
|
|
1742
2440
|
for (const file of sorted) {
|
|
1743
2441
|
if (totalChars >= maxTotalChars) break;
|
|
1744
|
-
const content =
|
|
2442
|
+
const content = readFileSafe2(join7(rootDir, file));
|
|
1745
2443
|
if (!content || content.trim().length < 20) continue;
|
|
1746
2444
|
if (isTestFile(file)) continue;
|
|
1747
2445
|
if (isGeneratedFile(file, content)) continue;
|
|
@@ -1760,8 +2458,7 @@ function getExtensionsForStack(techStack) {
|
|
|
1760
2458
|
return [".swift"];
|
|
1761
2459
|
case "kotlin":
|
|
1762
2460
|
return [".kt", ".kts"];
|
|
1763
|
-
case "
|
|
1764
|
-
return [".java"];
|
|
2461
|
+
case "expo":
|
|
1765
2462
|
case "react-native":
|
|
1766
2463
|
case "capacitor":
|
|
1767
2464
|
return [".ts", ".tsx", ".js", ".jsx"];
|
|
@@ -1792,11 +2489,30 @@ function getPriorityPatternsForStack(techStack) {
|
|
|
1792
2489
|
case "swift":
|
|
1793
2490
|
return [...common, "view", "controller", "manager", "delegate", "contentview"];
|
|
1794
2491
|
case "kotlin":
|
|
1795
|
-
|
|
1796
|
-
|
|
2492
|
+
return [
|
|
2493
|
+
...common,
|
|
2494
|
+
"activity",
|
|
2495
|
+
"fragment",
|
|
2496
|
+
"viewmodel",
|
|
2497
|
+
"repository",
|
|
2498
|
+
// Jetpack Compose deyimleri — Compose-first projelerde UI kodları
|
|
2499
|
+
// bunlar olmadan priority listesinde aşağı sıralanıyordu
|
|
2500
|
+
"composable",
|
|
2501
|
+
"screen",
|
|
2502
|
+
"theme",
|
|
2503
|
+
"navigation",
|
|
2504
|
+
"navgraph",
|
|
2505
|
+
"scaffold",
|
|
2506
|
+
"compose",
|
|
2507
|
+
"material"
|
|
2508
|
+
];
|
|
2509
|
+
case "expo":
|
|
2510
|
+
return [...common, "screen", "component", "hook", "context", "store", "slice", "_layout", "tabs"];
|
|
1797
2511
|
case "react-native":
|
|
1798
2512
|
case "capacitor":
|
|
1799
2513
|
return [...common, "screen", "component", "hook", "context", "store", "slice"];
|
|
2514
|
+
case "dotnet-maui":
|
|
2515
|
+
return [...common, "page", "view", "viewmodel", "service", "mauiprogram", "appshell"];
|
|
1800
2516
|
default:
|
|
1801
2517
|
return common;
|
|
1802
2518
|
}
|
|
@@ -1819,7 +2535,14 @@ function sortByPriority(files, patterns) {
|
|
|
1819
2535
|
}
|
|
1820
2536
|
function isTestFile(file) {
|
|
1821
2537
|
const lower = file.toLowerCase();
|
|
1822
|
-
|
|
2538
|
+
if (/(?:^|[/\\])(?:test|tests|__tests__|spec|specs|androidtest|mocks?|fixtures?)[/\\]/.test(
|
|
2539
|
+
lower
|
|
2540
|
+
)) {
|
|
2541
|
+
return true;
|
|
2542
|
+
}
|
|
2543
|
+
if (/\.(?:test|spec|mock|fixture)\.[a-z]+$/.test(lower)) return true;
|
|
2544
|
+
if (/(?:test|tests|spec|specs)\.[a-z]+$/.test(lower)) return true;
|
|
2545
|
+
return false;
|
|
1823
2546
|
}
|
|
1824
2547
|
function isGeneratedFile(file, content) {
|
|
1825
2548
|
const lower = file.toLowerCase();
|
|
@@ -1836,7 +2559,7 @@ function generateProjectTree(rootDir, maxDepth = 5, maxEntries = 300) {
|
|
|
1836
2559
|
if (depth > maxDepth || entryCount >= maxEntries) return;
|
|
1837
2560
|
let entries;
|
|
1838
2561
|
try {
|
|
1839
|
-
entries =
|
|
2562
|
+
entries = readdirSync7(dir).sort((a, b) => {
|
|
1840
2563
|
const aIsDir = isDir(join7(dir, a));
|
|
1841
2564
|
const bIsDir = isDir(join7(dir, b));
|
|
1842
2565
|
if (aIsDir && !bIsDir) return -1;
|
|
@@ -1885,7 +2608,7 @@ function generateProjectTree(rootDir, maxDepth = 5, maxEntries = 300) {
|
|
|
1885
2608
|
}
|
|
1886
2609
|
|
|
1887
2610
|
// src/analyzers/asset-scanner.ts
|
|
1888
|
-
import { readFileSync as readFileSync4, existsSync as existsSync5, readdirSync as
|
|
2611
|
+
import { readFileSync as readFileSync4, existsSync as existsSync5, readdirSync as readdirSync8, statSync as statSync5 } from "fs";
|
|
1889
2612
|
import { join as join8, extname as extname3, relative as relative3, basename as basename2 } from "path";
|
|
1890
2613
|
var MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
|
1891
2614
|
var MAX_TOTAL_ASSETS = 10;
|
|
@@ -1927,7 +2650,7 @@ function collectImagesRecursive(rootDir, dirPath, assetType, assets, maxCount, m
|
|
|
1927
2650
|
if (currentDepth > maxDepth) return;
|
|
1928
2651
|
if (!existsSync5(dirPath)) return;
|
|
1929
2652
|
try {
|
|
1930
|
-
const entries =
|
|
2653
|
+
const entries = readdirSync8(dirPath);
|
|
1931
2654
|
for (const entry of entries) {
|
|
1932
2655
|
if (assets.filter((a) => a.asset_type === assetType).length >= maxCount) return;
|
|
1933
2656
|
if (assets.length >= MAX_TOTAL_ASSETS) return;
|
|
@@ -1976,6 +2699,17 @@ function getScreenshotDirs(rootDir, techStack) {
|
|
|
1976
2699
|
gen(join8(rootDir, "assets"))
|
|
1977
2700
|
);
|
|
1978
2701
|
break;
|
|
2702
|
+
case "expo":
|
|
2703
|
+
dirs.push(
|
|
2704
|
+
ss(join8(rootDir, "assets/screenshots")),
|
|
2705
|
+
ss(join8(rootDir, "src/assets/screenshots")),
|
|
2706
|
+
ss(join8(rootDir, "docs/screenshots")),
|
|
2707
|
+
gen(join8(rootDir, "assets/images")),
|
|
2708
|
+
gen(join8(rootDir, "assets")),
|
|
2709
|
+
gen(join8(rootDir, "src/assets/images")),
|
|
2710
|
+
gen(join8(rootDir, "src/assets"))
|
|
2711
|
+
);
|
|
2712
|
+
break;
|
|
1979
2713
|
case "react-native":
|
|
1980
2714
|
case "capacitor":
|
|
1981
2715
|
dirs.push(
|
|
@@ -1996,7 +2730,6 @@ function getScreenshotDirs(rootDir, techStack) {
|
|
|
1996
2730
|
);
|
|
1997
2731
|
break;
|
|
1998
2732
|
case "kotlin":
|
|
1999
|
-
case "java":
|
|
2000
2733
|
dirs.push(
|
|
2001
2734
|
ss(join8(rootDir, "fastlane/metadata/android/en-US/images/phoneScreenshots")),
|
|
2002
2735
|
gen(join8(rootDir, "metadata/android/en-US/images")),
|
|
@@ -2004,6 +2737,15 @@ function getScreenshotDirs(rootDir, techStack) {
|
|
|
2004
2737
|
gen(join8(rootDir, "Marketing"))
|
|
2005
2738
|
);
|
|
2006
2739
|
break;
|
|
2740
|
+
case "dotnet-maui":
|
|
2741
|
+
dirs.push(
|
|
2742
|
+
ss(join8(rootDir, "Resources/Images/screenshots")),
|
|
2743
|
+
ss(join8(rootDir, "Resources/Screenshots")),
|
|
2744
|
+
gen(join8(rootDir, "Resources/Images")),
|
|
2745
|
+
gen(join8(rootDir, "marketing")),
|
|
2746
|
+
gen(join8(rootDir, "Marketing"))
|
|
2747
|
+
);
|
|
2748
|
+
break;
|
|
2007
2749
|
}
|
|
2008
2750
|
return dirs;
|
|
2009
2751
|
}
|
|
@@ -2030,6 +2772,31 @@ function getSplashPaths(rootDir, techStack) {
|
|
|
2030
2772
|
);
|
|
2031
2773
|
}
|
|
2032
2774
|
break;
|
|
2775
|
+
case "expo":
|
|
2776
|
+
paths.push(
|
|
2777
|
+
join8(rootDir, "assets/splash.png"),
|
|
2778
|
+
join8(rootDir, "assets/splash-icon.png"),
|
|
2779
|
+
join8(rootDir, "assets/images/splash.png"),
|
|
2780
|
+
join8(rootDir, "assets/images/splash-icon.png"),
|
|
2781
|
+
join8(rootDir, "src/assets/splash.png")
|
|
2782
|
+
);
|
|
2783
|
+
const expoSplashContent = readFileSafe(join8(rootDir, "app.json"));
|
|
2784
|
+
if (expoSplashContent) {
|
|
2785
|
+
try {
|
|
2786
|
+
const data = JSON.parse(expoSplashContent);
|
|
2787
|
+
const expo = data.expo || data;
|
|
2788
|
+
const candidates = [
|
|
2789
|
+
expo.splash?.image,
|
|
2790
|
+
expo.android?.splash?.image,
|
|
2791
|
+
expo.ios?.splash?.image
|
|
2792
|
+
].filter((v) => typeof v === "string");
|
|
2793
|
+
for (const rel of candidates) {
|
|
2794
|
+
paths.push(join8(rootDir, rel.replace(/^\.?\//, "")));
|
|
2795
|
+
}
|
|
2796
|
+
} catch {
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
break;
|
|
2033
2800
|
case "react-native":
|
|
2034
2801
|
case "capacitor":
|
|
2035
2802
|
paths.push(
|
|
@@ -2040,7 +2807,6 @@ function getSplashPaths(rootDir, techStack) {
|
|
|
2040
2807
|
);
|
|
2041
2808
|
break;
|
|
2042
2809
|
case "kotlin":
|
|
2043
|
-
case "java":
|
|
2044
2810
|
for (const density of ["xxxhdpi", "xxhdpi", "xhdpi"]) {
|
|
2045
2811
|
paths.push(
|
|
2046
2812
|
join8(rootDir, `app/src/main/res/drawable-${density}/splash.png`),
|
|
@@ -2048,6 +2814,14 @@ function getSplashPaths(rootDir, techStack) {
|
|
|
2048
2814
|
);
|
|
2049
2815
|
}
|
|
2050
2816
|
break;
|
|
2817
|
+
case "dotnet-maui":
|
|
2818
|
+
paths.push(
|
|
2819
|
+
join8(rootDir, "Resources/Splash/splash.png"),
|
|
2820
|
+
join8(rootDir, "Resources/Splash/splashscreen.png"),
|
|
2821
|
+
join8(rootDir, "Resources/Splash/splash.svg"),
|
|
2822
|
+
join8(rootDir, "Resources/Images/splash.png")
|
|
2823
|
+
);
|
|
2824
|
+
break;
|
|
2051
2825
|
}
|
|
2052
2826
|
return paths;
|
|
2053
2827
|
}
|
|
@@ -2060,19 +2834,26 @@ function getFeatureGraphicPaths(rootDir, techStack) {
|
|
|
2060
2834
|
);
|
|
2061
2835
|
switch (techStack) {
|
|
2062
2836
|
case "flutter":
|
|
2837
|
+
case "expo":
|
|
2063
2838
|
case "react-native":
|
|
2064
2839
|
case "capacitor":
|
|
2065
2840
|
paths.push(
|
|
2066
2841
|
join8(rootDir, "assets/feature_graphic.png"),
|
|
2067
|
-
join8(rootDir, "assets/feature-graphic.png")
|
|
2842
|
+
join8(rootDir, "assets/feature-graphic.png"),
|
|
2843
|
+
join8(rootDir, "assets/images/feature_graphic.png")
|
|
2068
2844
|
);
|
|
2069
2845
|
break;
|
|
2070
2846
|
case "kotlin":
|
|
2071
|
-
case "java":
|
|
2072
2847
|
paths.push(
|
|
2073
2848
|
join8(rootDir, "app/src/main/feature_graphic.png")
|
|
2074
2849
|
);
|
|
2075
2850
|
break;
|
|
2851
|
+
case "dotnet-maui":
|
|
2852
|
+
paths.push(
|
|
2853
|
+
join8(rootDir, "Resources/Images/feature_graphic.png"),
|
|
2854
|
+
join8(rootDir, "Resources/Images/feature-graphic.png")
|
|
2855
|
+
);
|
|
2856
|
+
break;
|
|
2076
2857
|
}
|
|
2077
2858
|
return paths;
|
|
2078
2859
|
}
|
|
@@ -2607,7 +3388,7 @@ async function analyzeCommand(options) {
|
|
|
2607
3388
|
if (techStack.stack === "unknown") {
|
|
2608
3389
|
techSpinner.fail(
|
|
2609
3390
|
chalk.red(
|
|
2610
|
-
"No supported project found. Supported: Swift, Flutter, React Native, Kotlin, Capacitor, .NET MAUI"
|
|
3391
|
+
"No supported project found. Supported: Swift, Flutter, React Native, Expo, Kotlin, Capacitor, .NET MAUI"
|
|
2611
3392
|
)
|
|
2612
3393
|
);
|
|
2613
3394
|
process.exit(1);
|