vxrn 2.8.0 → 2.9.0
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/exports/clean.mjs +128 -19
- package/dist/exports/clean.mjs.map +1 -1
- package/dist/exports/clean.native.js +147 -21
- package/dist/exports/clean.native.js.map +1 -1
- package/dist/exports/prebuild.mjs +2 -1
- package/dist/exports/prebuild.mjs.map +1 -1
- package/dist/exports/prebuild.native.js +2 -1
- package/dist/exports/prebuild.native.js.map +1 -1
- package/dist/exports/prebuildWithoutExpo.mjs +196 -7
- package/dist/exports/prebuildWithoutExpo.mjs.map +1 -1
- package/dist/exports/prebuildWithoutExpo.native.js +280 -42
- package/dist/exports/prebuildWithoutExpo.native.js.map +1 -1
- package/dist/plugins/reactNativeDevServer.mjs +1 -1
- package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
- package/dist/plugins/reactNativeDevServer.native.js +1 -1
- package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
- package/dist/user-interface/index.mjs +31 -24
- package/dist/user-interface/index.mjs.map +1 -1
- package/dist/user-interface/index.native.js +32 -25
- package/dist/user-interface/index.native.js.map +1 -1
- package/dist/utils/createNativeDevEngine.mjs +111 -14
- package/dist/utils/createNativeDevEngine.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.native.js +143 -14
- package/dist/utils/createNativeDevEngine.native.js.map +1 -1
- package/dist/utils/scanDepsToOptimize.mjs +10 -6
- package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
- package/dist/utils/scanDepsToOptimize.native.js +12 -6
- package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
- package/expo-plugin.cjs +71 -8
- package/package.json +13 -13
- package/src/exports/clean.test.ts +502 -5
- package/src/exports/clean.ts +176 -19
- package/src/exports/expoPlugin.test.mjs +44 -1
- package/src/exports/prebuild.ts +6 -0
- package/src/exports/prebuildWithoutExpo.test.ts +134 -2
- package/src/exports/prebuildWithoutExpo.ts +235 -10
- package/src/plugins/reactNativeDevServer.ts +1 -1
- package/src/user-interface/index.ts +50 -42
- package/src/utils/createNativeDevEngine.test.ts +486 -7
- package/src/utils/createNativeDevEngine.ts +220 -17
- package/src/utils/imageSizePatch.test.ts +3 -3
- package/src/utils/scanDepsToOptimize.test.ts +35 -2
- package/src/utils/scanDepsToOptimize.ts +25 -8
- package/types/exports/clean.d.ts.map +1 -1
- package/types/exports/prebuild.d.ts +2 -1
- package/types/exports/prebuild.d.ts.map +1 -1
- package/types/exports/prebuildWithoutExpo.d.ts.map +1 -1
- package/types/user-interface/index.d.ts.map +1 -1
- package/types/utils/createNativeDevEngine.d.ts +1 -1
- package/types/utils/createNativeDevEngine.d.ts.map +1 -1
- package/types/utils/scanDepsToOptimize.d.ts +25 -0
- package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
|
@@ -4,7 +4,10 @@ import module from 'node:module'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { pathToFileURL } from 'node:url'
|
|
6
6
|
import {
|
|
7
|
+
androidIconSuffix,
|
|
8
|
+
appIconPreviewResources,
|
|
7
9
|
validateNativeApp,
|
|
10
|
+
type NativeAppIcon,
|
|
8
11
|
type NativeAppManifest,
|
|
9
12
|
type PlistValue,
|
|
10
13
|
} from '@vxrn/utils/nativeAppManifest'
|
|
@@ -1353,6 +1356,59 @@ const IPAD_APP_ICON_SIZES: Array<[string, string[]]> = [
|
|
|
1353
1356
|
['83.5x83.5', ['2x']],
|
|
1354
1357
|
]
|
|
1355
1358
|
|
|
1359
|
+
// ios 18 home screen appearances use the single 1024 universal entry, which
|
|
1360
|
+
// the asset compiler scales to every size, with a dark and a tinted variant
|
|
1361
|
+
// beside the light one.
|
|
1362
|
+
async function writeAppearanceIcon(args: {
|
|
1363
|
+
root: string
|
|
1364
|
+
iconDir: string
|
|
1365
|
+
icon: NativeAppIcon
|
|
1366
|
+
source: string
|
|
1367
|
+
}): Promise<void> {
|
|
1368
|
+
const { root, iconDir, icon, source } = args
|
|
1369
|
+
const variantSource = (variant: { source?: string } | undefined) => {
|
|
1370
|
+
if (!variant?.source) return source
|
|
1371
|
+
const resolved = path.resolve(root, variant.source)
|
|
1372
|
+
if (!FSExtra.existsSync(resolved)) {
|
|
1373
|
+
throw new Error(`[vxrn] native.app icon appearance source does not exist: ${resolved}`)
|
|
1374
|
+
}
|
|
1375
|
+
return resolved
|
|
1376
|
+
}
|
|
1377
|
+
const render = async (file: string, from: string, background?: string, gray = false) => {
|
|
1378
|
+
let image = sharp(from).rotate().resize(1024, 1024, { fit: 'cover' })
|
|
1379
|
+
if (background) image = image.flatten({ background })
|
|
1380
|
+
if (gray) image = image.greyscale()
|
|
1381
|
+
await image.png().toFile(path.join(iconDir, file))
|
|
1382
|
+
}
|
|
1383
|
+
const images: Array<Record<string, unknown>> = []
|
|
1384
|
+
const entry = (filename: string, value?: string) => ({
|
|
1385
|
+
...(value ? { appearances: [{ appearance: 'luminosity', value }] } : {}),
|
|
1386
|
+
filename,
|
|
1387
|
+
idiom: 'universal',
|
|
1388
|
+
platform: 'ios',
|
|
1389
|
+
size: '1024x1024',
|
|
1390
|
+
})
|
|
1391
|
+
await render('icon-1024.png', source, icon.backgroundColor)
|
|
1392
|
+
images.push(entry('icon-1024.png'))
|
|
1393
|
+
if (icon.dark) {
|
|
1394
|
+
await render('icon-1024-dark.png', variantSource(icon.dark), icon.dark.backgroundColor)
|
|
1395
|
+
images.push(entry('icon-1024-dark.png', 'dark'))
|
|
1396
|
+
}
|
|
1397
|
+
if (icon.tinted) {
|
|
1398
|
+
await render(
|
|
1399
|
+
'icon-1024-tinted.png',
|
|
1400
|
+
variantSource(icon.tinted),
|
|
1401
|
+
icon.tinted.backgroundColor ?? '#000000',
|
|
1402
|
+
true
|
|
1403
|
+
)
|
|
1404
|
+
images.push(entry('icon-1024-tinted.png', 'tinted'))
|
|
1405
|
+
}
|
|
1406
|
+
FSExtra.writeFileSync(
|
|
1407
|
+
path.join(iconDir, 'Contents.json'),
|
|
1408
|
+
`${JSON.stringify({ images, info: { author: 'xcode', version: 1 } }, null, 2)}\n`
|
|
1409
|
+
)
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1356
1412
|
async function generateAppIcons(args: {
|
|
1357
1413
|
root: string
|
|
1358
1414
|
dest: string
|
|
@@ -1368,9 +1424,9 @@ async function generateAppIcons(args: {
|
|
|
1368
1424
|
images: Array<{ idiom: string; scale: string; size: string; filename?: string }>
|
|
1369
1425
|
info: { author: string; version: number }
|
|
1370
1426
|
} = JSON.parse(FSExtra.readFileSync(path.join(assetsDir, 'AppIcon.appiconset', 'Contents.json'), 'utf8'))
|
|
1371
|
-
const icons: Array<[string,
|
|
1427
|
+
const icons: Array<[string, NativeAppIcon]> = [
|
|
1372
1428
|
['AppIcon', app.icon],
|
|
1373
|
-
...Object.entries(app.
|
|
1429
|
+
...Object.entries(app.alternateIcons ?? {}),
|
|
1374
1430
|
]
|
|
1375
1431
|
for (const [name, icon] of icons) {
|
|
1376
1432
|
const source = path.resolve(root, icon.source)
|
|
@@ -1383,6 +1439,10 @@ async function generateAppIcons(args: {
|
|
|
1383
1439
|
}
|
|
1384
1440
|
const iconDir = path.join(assetsDir, `${name}.appiconset`)
|
|
1385
1441
|
FSExtra.mkdirSync(iconDir, { recursive: true })
|
|
1442
|
+
if (icon.dark || icon.tinted) {
|
|
1443
|
+
await writeAppearanceIcon({ root, iconDir, icon, source })
|
|
1444
|
+
continue
|
|
1445
|
+
}
|
|
1386
1446
|
const contents = structuredClone(template)
|
|
1387
1447
|
// the template is iPhone-only. App Store upload rejects a tablet build
|
|
1388
1448
|
// without the iPad sizes (ITMS-90023, 152 and 167 pixels among them).
|
|
@@ -1418,17 +1478,172 @@ async function generateAppIcons(args: {
|
|
|
1418
1478
|
if (metadata.width !== metadata.height || metadata.width === undefined || metadata.width < 1024) {
|
|
1419
1479
|
throw new Error('[vxrn] native.app.icon source must be a square image at least 1024px wide')
|
|
1420
1480
|
}
|
|
1481
|
+
// the primary icon and each alternate's launcher icon, one legacy png per
|
|
1482
|
+
// density. an alternate's alias points at its own ic_launcher_<name> pair.
|
|
1483
|
+
const launchers: Array<[string, NativeAppIcon, string]> = [
|
|
1484
|
+
['ic_launcher', app.icon, source],
|
|
1485
|
+
...(await Promise.all(
|
|
1486
|
+
Object.entries(app.alternateIcons ?? {}).map(
|
|
1487
|
+
async ([name, icon]): Promise<[string, NativeAppIcon, string]> => {
|
|
1488
|
+
const alternateSource = path.resolve(root, icon.source)
|
|
1489
|
+
if (!FSExtra.existsSync(alternateSource)) {
|
|
1490
|
+
throw new Error(`[vxrn] native.app icon source does not exist: ${alternateSource}`)
|
|
1491
|
+
}
|
|
1492
|
+
const alternate = await sharp(alternateSource).metadata()
|
|
1493
|
+
if (alternate.width !== alternate.height || alternate.width === undefined || alternate.width < 1024) {
|
|
1494
|
+
throw new Error(`[vxrn] native.app icon source must be a square image at least 1024px wide: ${alternateSource}`)
|
|
1495
|
+
}
|
|
1496
|
+
return [androidAliasIconName(name), icon, alternateSource]
|
|
1497
|
+
}
|
|
1498
|
+
)
|
|
1499
|
+
)),
|
|
1500
|
+
]
|
|
1421
1501
|
for (const [density, multiplier] of Object.entries(ANDROID_DENSITIES)) {
|
|
1422
1502
|
const pixels = 48 * multiplier
|
|
1423
1503
|
const iconDir = path.join(dest, 'app', 'src', 'main', 'res', `mipmap-${density}`)
|
|
1424
|
-
for (const
|
|
1425
|
-
|
|
1504
|
+
for (const [resource, icon, iconSource] of launchers) {
|
|
1505
|
+
for (const filename of [`${resource}.png`, `${resource}_round.png`]) {
|
|
1506
|
+
await sharp(iconSource)
|
|
1507
|
+
.rotate()
|
|
1508
|
+
.resize(pixels, pixels, { fit: 'cover' })
|
|
1509
|
+
.flatten({ background: icon.backgroundColor })
|
|
1510
|
+
.png()
|
|
1511
|
+
.toFile(path.join(iconDir, filename))
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function androidAliasIconName(name: string): string {
|
|
1518
|
+
return `ic_launcher_${androidIconSuffix(name)}`
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const LAUNCHER_FILTER = /[ \t]*<intent-filter>(?:(?!<\/intent-filter>)[\s\S])*android\.intent\.category\.LAUNCHER[\s\S]*?<\/intent-filter>\n?/
|
|
1522
|
+
|
|
1523
|
+
// launcher aliases for One.AppIcon: the real MainActivity stays enabled and
|
|
1524
|
+
// loses its launcher filter to a Primary alias, each alternate is a disabled
|
|
1525
|
+
// alias, and every alias targets a small activity that forwards into the
|
|
1526
|
+
// host. android finishes activities launched through an alias when that alias
|
|
1527
|
+
// is disabled, so an alias must never target the react native host itself.
|
|
1528
|
+
function stampAndroidIconAliases(manifest: string, app: NativeAppManifest): string {
|
|
1529
|
+
const alternates = Object.keys(app.alternateIcons ?? {})
|
|
1530
|
+
const host = manifest.match(/<activity\b(?:(?!<\/activity>)[\s\S])*android:name="\.MainActivity"[\s\S]*?<\/activity>/)?.[0]
|
|
1531
|
+
if (!host || !LAUNCHER_FILTER.test(host)) {
|
|
1532
|
+
throw new Error(
|
|
1533
|
+
'[vxrn] cannot stamp alternate icons: expected the launcher filter on .MainActivity in app/src/main/AndroidManifest.xml'
|
|
1534
|
+
)
|
|
1535
|
+
}
|
|
1536
|
+
const alias = (name: string, icon: string, enabled: boolean) => ` <activity-alias
|
|
1537
|
+
android:name=".${name}"
|
|
1538
|
+
android:targetActivity=".AppIconLauncherActivity"
|
|
1539
|
+
android:enabled="${enabled}"
|
|
1540
|
+
android:exported="true"
|
|
1541
|
+
android:icon="@mipmap/${icon}"
|
|
1542
|
+
android:roundIcon="@mipmap/${icon}_round"
|
|
1543
|
+
android:label="@string/app_name">
|
|
1544
|
+
<intent-filter>
|
|
1545
|
+
<action android:name="android.intent.action.MAIN" />
|
|
1546
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
1547
|
+
</intent-filter>
|
|
1548
|
+
</activity-alias>`
|
|
1549
|
+
const stamps = [
|
|
1550
|
+
` <activity
|
|
1551
|
+
android:name=".AppIconLauncherActivity"
|
|
1552
|
+
android:exported="false"
|
|
1553
|
+
android:taskAffinity=""
|
|
1554
|
+
android:excludeFromRecents="true"
|
|
1555
|
+
android:theme="@android:style/Theme.NoDisplay" />`,
|
|
1556
|
+
alias('Primary', 'ic_launcher', true),
|
|
1557
|
+
...alternates.map((name) => alias(name, androidAliasIconName(name), false)),
|
|
1558
|
+
].join('\n')
|
|
1559
|
+
const anchor = ' </application>'
|
|
1560
|
+
if (!manifest.includes(anchor)) {
|
|
1561
|
+
throw new Error(
|
|
1562
|
+
'[vxrn] cannot stamp alternate icons: expected </application> in app/src/main/AndroidManifest.xml'
|
|
1563
|
+
)
|
|
1564
|
+
}
|
|
1565
|
+
return manifest
|
|
1566
|
+
.replace(host, host.replace(LAUNCHER_FILTER, ''))
|
|
1567
|
+
.replace(anchor, `${stamps}\n${anchor}`)
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function generateAndroidIconForwarder(dest: string, app: NativeAppManifest): void {
|
|
1571
|
+
if (!app.alternateIcons || !app.android) return
|
|
1572
|
+
const packageName = app.android.applicationId
|
|
1573
|
+
const file = path.join(
|
|
1574
|
+
dest,
|
|
1575
|
+
'app',
|
|
1576
|
+
'src',
|
|
1577
|
+
'main',
|
|
1578
|
+
'java',
|
|
1579
|
+
...packageName.split('.'),
|
|
1580
|
+
'AppIconLauncherActivity.kt'
|
|
1581
|
+
)
|
|
1582
|
+
FSExtra.mkdirSync(path.dirname(file), { recursive: true })
|
|
1583
|
+
FSExtra.writeFileSync(
|
|
1584
|
+
file,
|
|
1585
|
+
`package ${packageName}
|
|
1586
|
+
|
|
1587
|
+
import android.app.Activity
|
|
1588
|
+
import android.content.Intent
|
|
1589
|
+
import android.os.Bundle
|
|
1590
|
+
|
|
1591
|
+
// launcher aliases finish before react native mounts in the permanent host.
|
|
1592
|
+
class AppIconLauncherActivity : Activity() {
|
|
1593
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
1594
|
+
super.onCreate(savedInstanceState)
|
|
1595
|
+
startActivity(Intent(intent).setClass(this, MainActivity::class.java)
|
|
1596
|
+
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
|
1597
|
+
Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
|
1598
|
+
finish()
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
`
|
|
1602
|
+
)
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// a 64 point preview of the primary icon and each alternate, bundled as plain
|
|
1606
|
+
// images so an in-app picker (One.AppIcon.list) needs no second copy of the
|
|
1607
|
+
// art: an imageset on ios, which cannot load an app icon set by name, and a
|
|
1608
|
+
// density-independent drawable on android.
|
|
1609
|
+
async function generateAppIconPreviews(args: {
|
|
1610
|
+
root: string
|
|
1611
|
+
dest: string
|
|
1612
|
+
platform: 'ios' | 'android'
|
|
1613
|
+
app: NativeAppManifest
|
|
1614
|
+
}): Promise<void> {
|
|
1615
|
+
const { root, dest, platform, app } = args
|
|
1616
|
+
if (!app.icon || !app.alternateIcons) return
|
|
1617
|
+
const icons: Array<[string | undefined, NativeAppIcon]> = [
|
|
1618
|
+
[undefined, app.icon],
|
|
1619
|
+
...Object.entries(app.alternateIcons),
|
|
1620
|
+
]
|
|
1621
|
+
for (const [name, icon] of icons) {
|
|
1622
|
+
const preview = (scale: number) =>
|
|
1623
|
+
sharp(path.resolve(root, icon.source))
|
|
1426
1624
|
.rotate()
|
|
1427
|
-
.resize(
|
|
1428
|
-
.flatten({ background:
|
|
1625
|
+
.resize(64 * scale, 64 * scale, { fit: 'cover' })
|
|
1626
|
+
.flatten({ background: icon.backgroundColor })
|
|
1429
1627
|
.png()
|
|
1430
|
-
|
|
1628
|
+
const resources = appIconPreviewResources(name)
|
|
1629
|
+
if (platform === 'android') {
|
|
1630
|
+
const dir = path.join(dest, 'app', 'src', 'main', 'res', 'drawable-nodpi')
|
|
1631
|
+
FSExtra.mkdirSync(dir, { recursive: true })
|
|
1632
|
+
await preview(3).toFile(path.join(dir, `${resources.android}.png`))
|
|
1633
|
+
continue
|
|
1431
1634
|
}
|
|
1635
|
+
const dir = path.join(dest, app.name, 'Images.xcassets', `${resources.ios}.imageset`)
|
|
1636
|
+
FSExtra.mkdirSync(dir, { recursive: true })
|
|
1637
|
+
const images: Array<Record<string, string>> = []
|
|
1638
|
+
for (const scale of [1, 2, 3]) {
|
|
1639
|
+
const filename = `preview@${scale}x.png`
|
|
1640
|
+
await preview(scale).toFile(path.join(dir, filename))
|
|
1641
|
+
images.push({ filename, idiom: 'universal', scale: `${scale}x` })
|
|
1642
|
+
}
|
|
1643
|
+
FSExtra.writeFileSync(
|
|
1644
|
+
path.join(dir, 'Contents.json'),
|
|
1645
|
+
`${JSON.stringify({ images, info: { author: 'xcode', version: 1 } }, null, 2)}\n`
|
|
1646
|
+
)
|
|
1432
1647
|
}
|
|
1433
1648
|
}
|
|
1434
1649
|
|
|
@@ -1855,7 +2070,8 @@ export function renderPrebuildFile(args: {
|
|
|
1855
2070
|
app: NativeAppManifest
|
|
1856
2071
|
nitroWebImage?: boolean
|
|
1857
2072
|
}): RenderedPrebuildFile {
|
|
1858
|
-
const {
|
|
2073
|
+
const { content, platform, app, nitroWebImage } = args
|
|
2074
|
+
const relativePath = args.relativePath.replace(/\\/g, '/')
|
|
1859
2075
|
const appName = app.name
|
|
1860
2076
|
const schemes =
|
|
1861
2077
|
app.scheme === undefined ? [] : Array.isArray(app.scheme) ? app.scheme : [app.scheme]
|
|
@@ -2493,6 +2709,13 @@ ${schemes.map((scheme) => ` <data android:scheme="${scheme}" />`).joi
|
|
|
2493
2709
|
</activity>`
|
|
2494
2710
|
)
|
|
2495
2711
|
}
|
|
2712
|
+
if (
|
|
2713
|
+
platform === 'android' &&
|
|
2714
|
+
relativePath === 'app/src/main/AndroidManifest.xml' &&
|
|
2715
|
+
app.alternateIcons !== undefined
|
|
2716
|
+
) {
|
|
2717
|
+
rendered = stampAndroidIconAliases(rendered, app)
|
|
2718
|
+
}
|
|
2496
2719
|
if (platform === 'ios' && relativePath.endsWith('.xcodeproj/project.pbxproj')) {
|
|
2497
2720
|
rendered = rendered.replace(
|
|
2498
2721
|
/TARGETED_DEVICE_FAMILY = "1,2";/g,
|
|
@@ -2518,12 +2741,12 @@ ${schemes.map((scheme) => ` <data android:scheme="${scheme}" />`).joi
|
|
|
2518
2741
|
}
|
|
2519
2742
|
}
|
|
2520
2743
|
if (platform === 'ios' && relativePath.endsWith('.xcodeproj/project.pbxproj') &&
|
|
2521
|
-
app.
|
|
2744
|
+
app.alternateIcons !== undefined) {
|
|
2522
2745
|
const anchor = 'ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;'
|
|
2523
2746
|
if (!rendered.includes(anchor)) {
|
|
2524
2747
|
throw new Error('[vxrn] prebuild template lost its primary app icon build setting')
|
|
2525
2748
|
}
|
|
2526
|
-
const names = Object.keys(app.
|
|
2749
|
+
const names = Object.keys(app.alternateIcons).sort().join(' ')
|
|
2527
2750
|
rendered = rendered.replaceAll(anchor,
|
|
2528
2751
|
`${anchor}\n\t\t\t\tASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "${names}";`)
|
|
2529
2752
|
}
|
|
@@ -2879,6 +3102,7 @@ export const generateForPlatform = async (
|
|
|
2879
3102
|
}
|
|
2880
3103
|
|
|
2881
3104
|
await generateAppIcons({ root, dest, platform, app })
|
|
3105
|
+
await generateAppIconPreviews({ root, dest, platform, app })
|
|
2882
3106
|
if (platform === 'android') await generateAdaptiveIcon({ root, dest, app })
|
|
2883
3107
|
await generateSplashScreen({ root, dest, platform, app })
|
|
2884
3108
|
if (platform === 'ios') generateAccentColor({ dest, app })
|
|
@@ -2888,6 +3112,7 @@ export const generateForPlatform = async (
|
|
|
2888
3112
|
if (platform === 'ios') generateOneBridgingHeader(dest, app)
|
|
2889
3113
|
if (platform === 'ios') generateIosWidgets(dest, app)
|
|
2890
3114
|
if (platform === 'ios') await generateSwiftPackages({ root, dest })
|
|
3115
|
+
if (platform === 'android') generateAndroidIconForwarder(dest, app)
|
|
2891
3116
|
if (platform === 'android') await generateKotlinSources({ root, dest })
|
|
2892
3117
|
}
|
|
2893
3118
|
|
|
@@ -116,7 +116,7 @@ export function createReactNativeDevServerPlugin(
|
|
|
116
116
|
let compileBytecode: ReturnType<typeof nativeDevBytecodeCompiler> | undefined
|
|
117
117
|
|
|
118
118
|
const devToolsSocketEndpoints = ['/inspector/device', '/inspector/debug']
|
|
119
|
-
const reactNativeDevToolsUrl = `http://${host}:${getBoundPort(server)}`
|
|
119
|
+
const reactNativeDevToolsUrl = `http://${typeof host === 'string' && host !== '0.0.0.0' ? host : 'localhost'}:${getBoundPort(server)}`
|
|
120
120
|
const { middleware, websocketEndpoints } = createDevMiddleware({
|
|
121
121
|
serverBaseUrl: reactNativeDevToolsUrl,
|
|
122
122
|
logger: console,
|
|
@@ -60,57 +60,65 @@ const COMMANDS = [
|
|
|
60
60
|
keys: 'dt',
|
|
61
61
|
label: 'open React Native DevTools',
|
|
62
62
|
terminalLabel: 'open React Native \x1b[1mD\x1b[0mev\x1b[1mT\x1b[0mools',
|
|
63
|
-
action:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// url.searchParams.set('appId', );
|
|
71
|
-
// url.searchParams.set('device', );
|
|
72
|
-
// url.searchParams.set('target', );
|
|
73
|
-
|
|
74
|
-
// The `/open-debugger` endpoint may not respond, so we don't wait for it and will ignore timeout errors
|
|
75
|
-
;(async () => {
|
|
76
|
-
const response = await fetch(url, {
|
|
77
|
-
method: 'POST',
|
|
78
|
-
signal: AbortSignal.timeout(3000),
|
|
79
|
-
}).catch((error) => {
|
|
80
|
-
if (error.name === 'TimeoutError') {
|
|
81
|
-
return null
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
throw error
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
if (!response) {
|
|
88
|
-
// This is common for now, so don't log it
|
|
89
|
-
// console.info(`No response received from the React Native DevTools.`)
|
|
90
|
-
} else if (response.ok === false) {
|
|
91
|
-
const responseText = await response.text()
|
|
92
|
-
|
|
93
|
-
if (responseText.includes('Unable to find debugger target')) {
|
|
94
|
-
// Will already print "No compatible apps connected. React Native DevTools can only be used with the Hermes engine.", so no need to warn again
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
console.warn(
|
|
99
|
-
`Failed to open React Native DevTools, ${url} returns ${response.status}: ${responseText}.`
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
})()
|
|
103
|
-
},
|
|
63
|
+
action: openReactNativeDevTools,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
keys: 'j',
|
|
67
|
+
label: 'open React Native DevTools',
|
|
68
|
+
terminalLabel: 'open React Native DevTools',
|
|
69
|
+
action: openReactNativeDevTools,
|
|
104
70
|
},
|
|
105
71
|
|
|
106
72
|
{
|
|
107
73
|
keys: '?',
|
|
108
|
-
label: '
|
|
74
|
+
label: 'show commands',
|
|
109
75
|
terminalLabel: 'show this menu',
|
|
110
76
|
action: async (ctx) => {},
|
|
111
77
|
},
|
|
112
78
|
] satisfies Command[]
|
|
113
79
|
|
|
80
|
+
function openReactNativeDevTools(ctx: Context) {
|
|
81
|
+
const { host } = ctx.server.config.server
|
|
82
|
+
const serverUrl = `http://${typeof host === 'string' && !!host ? host : 'localhost'}:${getBoundPort(ctx.server)}`
|
|
83
|
+
const url = new URL('/open-debugger', serverUrl)
|
|
84
|
+
|
|
85
|
+
// TODO: Seems to need these if multiple devices are connected, but haven't figured out how to pass these yet
|
|
86
|
+
// Currently will just launch DevTools for most recently connected device
|
|
87
|
+
// url.searchParams.set('appId', );
|
|
88
|
+
// url.searchParams.set('device', );
|
|
89
|
+
// url.searchParams.set('target', );
|
|
90
|
+
|
|
91
|
+
// The `/open-debugger` endpoint may not respond, so we don't wait for it and will ignore timeout errors
|
|
92
|
+
;(async () => {
|
|
93
|
+
const response = await fetch(url, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
signal: AbortSignal.timeout(3000),
|
|
96
|
+
}).catch((error) => {
|
|
97
|
+
if (error.name === 'TimeoutError') {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw error
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
if (!response) {
|
|
105
|
+
// This is common for now, so don't log it
|
|
106
|
+
// console.info(`No response received from the React Native DevTools.`)
|
|
107
|
+
} else if (response.ok === false) {
|
|
108
|
+
const responseText = await response.text()
|
|
109
|
+
|
|
110
|
+
if (responseText.includes('Unable to find debugger target')) {
|
|
111
|
+
// Will already print "No compatible apps connected. React Native DevTools can only be used with the Hermes engine.", so no need to warn again
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.warn(
|
|
116
|
+
`Failed to open React Native DevTools, ${url} returns ${response.status}: ${responseText}.`
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
})()
|
|
120
|
+
}
|
|
121
|
+
|
|
114
122
|
export async function startUserInterface(context: Context) {
|
|
115
123
|
if (!supportsKeyboardCommands()) {
|
|
116
124
|
return
|