vxrn 2.8.1 → 2.9.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.
Files changed (54) hide show
  1. package/dist/exports/clean.mjs +128 -19
  2. package/dist/exports/clean.mjs.map +1 -1
  3. package/dist/exports/clean.native.js +147 -21
  4. package/dist/exports/clean.native.js.map +1 -1
  5. package/dist/exports/prebuild.mjs +2 -1
  6. package/dist/exports/prebuild.mjs.map +1 -1
  7. package/dist/exports/prebuild.native.js +2 -1
  8. package/dist/exports/prebuild.native.js.map +1 -1
  9. package/dist/exports/prebuildWithoutExpo.mjs +196 -7
  10. package/dist/exports/prebuildWithoutExpo.mjs.map +1 -1
  11. package/dist/exports/prebuildWithoutExpo.native.js +280 -42
  12. package/dist/exports/prebuildWithoutExpo.native.js.map +1 -1
  13. package/dist/plugins/reactNativeDevServer.mjs +43 -8
  14. package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
  15. package/dist/plugins/reactNativeDevServer.native.js +45 -8
  16. package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
  17. package/dist/user-interface/index.mjs +31 -24
  18. package/dist/user-interface/index.mjs.map +1 -1
  19. package/dist/user-interface/index.native.js +32 -25
  20. package/dist/user-interface/index.native.js.map +1 -1
  21. package/dist/utils/createNativeDevEngine.mjs +111 -14
  22. package/dist/utils/createNativeDevEngine.mjs.map +1 -1
  23. package/dist/utils/createNativeDevEngine.native.js +143 -14
  24. package/dist/utils/createNativeDevEngine.native.js.map +1 -1
  25. package/dist/utils/scanDepsToOptimize.mjs +10 -6
  26. package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
  27. package/dist/utils/scanDepsToOptimize.native.js +12 -6
  28. package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
  29. package/expo-plugin.cjs +71 -8
  30. package/package.json +14 -14
  31. package/src/exports/clean.test.ts +502 -5
  32. package/src/exports/clean.ts +176 -19
  33. package/src/exports/expoPlugin.test.mjs +44 -1
  34. package/src/exports/prebuild.ts +6 -0
  35. package/src/exports/prebuildWithoutExpo.test.ts +134 -2
  36. package/src/exports/prebuildWithoutExpo.ts +235 -10
  37. package/src/plugins/reactNativeDevServer.test.ts +75 -1
  38. package/src/plugins/reactNativeDevServer.ts +45 -8
  39. package/src/user-interface/index.ts +50 -42
  40. package/src/utils/createNativeDevEngine.test.ts +480 -1
  41. package/src/utils/createNativeDevEngine.ts +220 -17
  42. package/src/utils/imageSizePatch.test.ts +3 -3
  43. package/src/utils/scanDepsToOptimize.test.ts +35 -2
  44. package/src/utils/scanDepsToOptimize.ts +25 -8
  45. package/types/exports/clean.d.ts.map +1 -1
  46. package/types/exports/prebuild.d.ts +2 -1
  47. package/types/exports/prebuild.d.ts.map +1 -1
  48. package/types/exports/prebuildWithoutExpo.d.ts.map +1 -1
  49. package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
  50. package/types/user-interface/index.d.ts.map +1 -1
  51. package/types/utils/createNativeDevEngine.d.ts +1 -1
  52. package/types/utils/createNativeDevEngine.d.ts.map +1 -1
  53. package/types/utils/scanDepsToOptimize.d.ts +25 -0
  54. package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import module from "node:module";
4
4
  import path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
- import { validateNativeApp } from "@vxrn/utils/nativeAppManifest";
6
+ import { androidIconSuffix, appIconPreviewResources, validateNativeApp } from "@vxrn/utils/nativeAppManifest";
7
7
  import FSExtra from "fs-extra";
8
8
  import sharp from "sharp";
9
9
  import { swiftPackageDirectories } from "../utils/swiftPackageId.mjs";
@@ -1042,13 +1042,59 @@ const IPAD_APP_ICON_SIZES = [
1042
1042
  ["76x76", ["1x", "2x"]],
1043
1043
  ["83.5x83.5", ["2x"]]
1044
1044
  ];
1045
+ async function writeAppearanceIcon(args) {
1046
+ const { root, iconDir, icon, source } = args;
1047
+ const variantSource = (variant) => {
1048
+ if (!variant?.source) return source;
1049
+ const resolved = path.resolve(root, variant.source);
1050
+ if (!FSExtra.existsSync(resolved)) {
1051
+ throw new Error(`[vxrn] native.app icon appearance source does not exist: ${resolved}`);
1052
+ }
1053
+ return resolved;
1054
+ };
1055
+ const render = async (file, from, background, gray = false) => {
1056
+ let image = sharp(from).rotate().resize(1024, 1024, { fit: "cover" });
1057
+ if (background) image = image.flatten({ background });
1058
+ if (gray) image = image.greyscale();
1059
+ await image.png().toFile(path.join(iconDir, file));
1060
+ };
1061
+ const images = [];
1062
+ const entry = (filename, value) => ({
1063
+ ...value ? { appearances: [{
1064
+ appearance: "luminosity",
1065
+ value
1066
+ }] } : {},
1067
+ filename,
1068
+ idiom: "universal",
1069
+ platform: "ios",
1070
+ size: "1024x1024"
1071
+ });
1072
+ await render("icon-1024.png", source, icon.backgroundColor);
1073
+ images.push(entry("icon-1024.png"));
1074
+ if (icon.dark) {
1075
+ await render("icon-1024-dark.png", variantSource(icon.dark), icon.dark.backgroundColor);
1076
+ images.push(entry("icon-1024-dark.png", "dark"));
1077
+ }
1078
+ if (icon.tinted) {
1079
+ await render("icon-1024-tinted.png", variantSource(icon.tinted), icon.tinted.backgroundColor ?? "#000000", true);
1080
+ images.push(entry("icon-1024-tinted.png", "tinted"));
1081
+ }
1082
+ FSExtra.writeFileSync(path.join(iconDir, "Contents.json"), `${JSON.stringify({
1083
+ images,
1084
+ info: {
1085
+ author: "xcode",
1086
+ version: 1
1087
+ }
1088
+ }, null, 2)}
1089
+ `);
1090
+ }
1045
1091
  async function generateAppIcons(args) {
1046
1092
  const { root, dest, platform, app } = args;
1047
1093
  if (!app.icon) return;
1048
1094
  if (platform === "ios") {
1049
1095
  const assetsDir = path.join(dest, app.name, "Images.xcassets");
1050
1096
  const template = JSON.parse(FSExtra.readFileSync(path.join(assetsDir, "AppIcon.appiconset", "Contents.json"), "utf8"));
1051
- const icons = [["AppIcon", app.icon], ...Object.entries(app.ios?.alternateIcons ?? {})];
1097
+ const icons = [["AppIcon", app.icon], ...Object.entries(app.alternateIcons ?? {})];
1052
1098
  for (const [name, icon] of icons) {
1053
1099
  const source2 = path.resolve(root, icon.source);
1054
1100
  if (!FSExtra.existsSync(source2)) {
@@ -1060,6 +1106,15 @@ async function generateAppIcons(args) {
1060
1106
  }
1061
1107
  const iconDir = path.join(assetsDir, `${name}.appiconset`);
1062
1108
  FSExtra.mkdirSync(iconDir, { recursive: true });
1109
+ if (icon.dark || icon.tinted) {
1110
+ await writeAppearanceIcon({
1111
+ root,
1112
+ iconDir,
1113
+ icon,
1114
+ source: source2
1115
+ });
1116
+ continue;
1117
+ }
1063
1118
  const contents = structuredClone(template);
1064
1119
  if (app.ios?.tablet) {
1065
1120
  for (const [size, scales] of IPAD_APP_ICON_SIZES) {
@@ -1091,12 +1146,135 @@ async function generateAppIcons(args) {
1091
1146
  if (metadata.width !== metadata.height || metadata.width === void 0 || metadata.width < 1024) {
1092
1147
  throw new Error("[vxrn] native.app.icon source must be a square image at least 1024px wide");
1093
1148
  }
1149
+ const launchers = [[
1150
+ "ic_launcher",
1151
+ app.icon,
1152
+ source
1153
+ ], ...await Promise.all(Object.entries(app.alternateIcons ?? {}).map(async ([name, icon]) => {
1154
+ const alternateSource = path.resolve(root, icon.source);
1155
+ if (!FSExtra.existsSync(alternateSource)) {
1156
+ throw new Error(`[vxrn] native.app icon source does not exist: ${alternateSource}`);
1157
+ }
1158
+ const alternate = await sharp(alternateSource).metadata();
1159
+ if (alternate.width !== alternate.height || alternate.width === void 0 || alternate.width < 1024) {
1160
+ throw new Error(`[vxrn] native.app icon source must be a square image at least 1024px wide: ${alternateSource}`);
1161
+ }
1162
+ return [
1163
+ androidAliasIconName(name),
1164
+ icon,
1165
+ alternateSource
1166
+ ];
1167
+ }))];
1094
1168
  for (const [density, multiplier] of Object.entries(ANDROID_DENSITIES)) {
1095
1169
  const pixels = 48 * multiplier;
1096
1170
  const iconDir = path.join(dest, "app", "src", "main", "res", `mipmap-${density}`);
1097
- for (const filename of ["ic_launcher.png", "ic_launcher_round.png"]) {
1098
- await sharp(source).rotate().resize(pixels, pixels, { fit: "cover" }).flatten({ background: app.icon.backgroundColor }).png().toFile(path.join(iconDir, filename));
1171
+ for (const [resource, icon, iconSource] of launchers) {
1172
+ for (const filename of [`${resource}.png`, `${resource}_round.png`]) {
1173
+ await sharp(iconSource).rotate().resize(pixels, pixels, { fit: "cover" }).flatten({ background: icon.backgroundColor }).png().toFile(path.join(iconDir, filename));
1174
+ }
1175
+ }
1176
+ }
1177
+ }
1178
+ function androidAliasIconName(name) {
1179
+ return `ic_launcher_${androidIconSuffix(name)}`;
1180
+ }
1181
+ const LAUNCHER_FILTER = /[ \t]*<intent-filter>(?:(?!<\/intent-filter>)[\s\S])*android\.intent\.category\.LAUNCHER[\s\S]*?<\/intent-filter>\n?/;
1182
+ function stampAndroidIconAliases(manifest, app) {
1183
+ const alternates = Object.keys(app.alternateIcons ?? {});
1184
+ const host = manifest.match(/<activity\b(?:(?!<\/activity>)[\s\S])*android:name="\.MainActivity"[\s\S]*?<\/activity>/)?.[0];
1185
+ if (!host || !LAUNCHER_FILTER.test(host)) {
1186
+ throw new Error("[vxrn] cannot stamp alternate icons: expected the launcher filter on .MainActivity in app/src/main/AndroidManifest.xml");
1187
+ }
1188
+ const alias = (name, icon, enabled) => ` <activity-alias
1189
+ android:name=".${name}"
1190
+ android:targetActivity=".AppIconLauncherActivity"
1191
+ android:enabled="${enabled}"
1192
+ android:exported="true"
1193
+ android:icon="@mipmap/${icon}"
1194
+ android:roundIcon="@mipmap/${icon}_round"
1195
+ android:label="@string/app_name">
1196
+ <intent-filter>
1197
+ <action android:name="android.intent.action.MAIN" />
1198
+ <category android:name="android.intent.category.LAUNCHER" />
1199
+ </intent-filter>
1200
+ </activity-alias>`;
1201
+ const stamps = [
1202
+ ` <activity
1203
+ android:name=".AppIconLauncherActivity"
1204
+ android:exported="false"
1205
+ android:taskAffinity=""
1206
+ android:excludeFromRecents="true"
1207
+ android:theme="@android:style/Theme.NoDisplay" />`,
1208
+ alias("Primary", "ic_launcher", true),
1209
+ ...alternates.map((name) => alias(name, androidAliasIconName(name), false))
1210
+ ].join("\n");
1211
+ const anchor = " </application>";
1212
+ if (!manifest.includes(anchor)) {
1213
+ throw new Error("[vxrn] cannot stamp alternate icons: expected </application> in app/src/main/AndroidManifest.xml");
1214
+ }
1215
+ return manifest.replace(host, host.replace(LAUNCHER_FILTER, "")).replace(anchor, `${stamps}
1216
+ ${anchor}`);
1217
+ }
1218
+ function generateAndroidIconForwarder(dest, app) {
1219
+ if (!app.alternateIcons || !app.android) return;
1220
+ const packageName = app.android.applicationId;
1221
+ const file = path.join(dest, "app", "src", "main", "java", ...packageName.split("."), "AppIconLauncherActivity.kt");
1222
+ FSExtra.mkdirSync(path.dirname(file), { recursive: true });
1223
+ FSExtra.writeFileSync(file, `package ${packageName}
1224
+
1225
+ import android.app.Activity
1226
+ import android.content.Intent
1227
+ import android.os.Bundle
1228
+
1229
+ // launcher aliases finish before react native mounts in the permanent host.
1230
+ class AppIconLauncherActivity : Activity() {
1231
+ override fun onCreate(savedInstanceState: Bundle?) {
1232
+ super.onCreate(savedInstanceState)
1233
+ startActivity(Intent(intent).setClass(this, MainActivity::class.java)
1234
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or
1235
+ Intent.FLAG_ACTIVITY_SINGLE_TOP))
1236
+ finish()
1237
+ }
1238
+ }
1239
+ `);
1240
+ }
1241
+ async function generateAppIconPreviews(args) {
1242
+ const { root, dest, platform, app } = args;
1243
+ if (!app.icon || !app.alternateIcons) return;
1244
+ const icons = [[void 0, app.icon], ...Object.entries(app.alternateIcons)];
1245
+ for (const [name, icon] of icons) {
1246
+ const preview = (scale) => sharp(path.resolve(root, icon.source)).rotate().resize(64 * scale, 64 * scale, { fit: "cover" }).flatten({ background: icon.backgroundColor }).png();
1247
+ const resources = appIconPreviewResources(name);
1248
+ if (platform === "android") {
1249
+ const dir2 = path.join(dest, "app", "src", "main", "res", "drawable-nodpi");
1250
+ FSExtra.mkdirSync(dir2, { recursive: true });
1251
+ await preview(3).toFile(path.join(dir2, `${resources.android}.png`));
1252
+ continue;
1099
1253
  }
1254
+ const dir = path.join(dest, app.name, "Images.xcassets", `${resources.ios}.imageset`);
1255
+ FSExtra.mkdirSync(dir, { recursive: true });
1256
+ const images = [];
1257
+ for (const scale of [
1258
+ 1,
1259
+ 2,
1260
+ 3
1261
+ ]) {
1262
+ const filename = `preview@${scale}x.png`;
1263
+ await preview(scale).toFile(path.join(dir, filename));
1264
+ images.push({
1265
+ filename,
1266
+ idiom: "universal",
1267
+ scale: `${scale}x`
1268
+ });
1269
+ }
1270
+ FSExtra.writeFileSync(path.join(dir, "Contents.json"), `${JSON.stringify({
1271
+ images,
1272
+ info: {
1273
+ author: "xcode",
1274
+ version: 1
1275
+ }
1276
+ }, null, 2)}
1277
+ `);
1100
1278
  }
1101
1279
  }
1102
1280
  async function generateAdaptiveIcon(args) {
@@ -1402,7 +1580,8 @@ ${background ? ` <item>
1402
1580
  </style>`));
1403
1581
  }
1404
1582
  function renderPrebuildFile(args) {
1405
- const { relativePath, content, platform, app, nitroWebImage } = args;
1583
+ const { content, platform, app, nitroWebImage } = args;
1584
+ const relativePath = args.relativePath.replace(/\\/g, "/");
1406
1585
  const appName = app.name;
1407
1586
  const schemes = app.scheme === void 0 ? [] : Array.isArray(app.scheme) ? app.scheme : [app.scheme];
1408
1587
  let destRelativePath = transformPath(relativePath);
@@ -1787,6 +1966,9 @@ ${schemes.map((scheme) => ` <data android:scheme="${scheme}" />`).joi
1787
1966
  </intent-filter>
1788
1967
  </activity>`);
1789
1968
  }
1969
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.alternateIcons !== void 0) {
1970
+ rendered = stampAndroidIconAliases(rendered, app);
1971
+ }
1790
1972
  if (platform === "ios" && relativePath.endsWith(".xcodeproj/project.pbxproj")) {
1791
1973
  rendered = rendered.replace(/TARGETED_DEVICE_FAMILY = "1,2";/g, `TARGETED_DEVICE_FAMILY = "${app.ios?.tablet ? "1,2" : "1"}";`);
1792
1974
  }
@@ -1798,12 +1980,12 @@ ${schemes.map((scheme) => ` <data android:scheme="${scheme}" />`).joi
1798
1980
  rendered = rendered.replace(/CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${app.ios.buildNumber};`);
1799
1981
  }
1800
1982
  }
1801
- if (platform === "ios" && relativePath.endsWith(".xcodeproj/project.pbxproj") && app.ios?.alternateIcons !== void 0) {
1983
+ if (platform === "ios" && relativePath.endsWith(".xcodeproj/project.pbxproj") && app.alternateIcons !== void 0) {
1802
1984
  const anchor = "ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;";
1803
1985
  if (!rendered.includes(anchor)) {
1804
1986
  throw new Error("[vxrn] prebuild template lost its primary app icon build setting");
1805
1987
  }
1806
- const names = Object.keys(app.ios.alternateIcons).sort().join(" ");
1988
+ const names = Object.keys(app.alternateIcons).sort().join(" ");
1807
1989
  rendered = rendered.replaceAll(anchor, `${anchor}
1808
1990
  ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "${names}";`);
1809
1991
  }
@@ -2073,6 +2255,12 @@ const generateForPlatform = async (root, platform, app, outDir = path.resolve(ro
2073
2255
  platform,
2074
2256
  app
2075
2257
  });
2258
+ await generateAppIconPreviews({
2259
+ root,
2260
+ dest,
2261
+ platform,
2262
+ app
2263
+ });
2076
2264
  if (platform === "android") await generateAdaptiveIcon({
2077
2265
  root,
2078
2266
  dest,
@@ -2106,6 +2294,7 @@ const generateForPlatform = async (root, platform, app, outDir = path.resolve(ro
2106
2294
  root,
2107
2295
  dest
2108
2296
  });
2297
+ if (platform === "android") generateAndroidIconForwarder(dest, app);
2109
2298
  if (platform === "android") await generateKotlinSources({
2110
2299
  root,
2111
2300
  dest