@tamagui/native-ci 1.144.1 → 1.144.3

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.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Android-specific utilities for Detox test runners
3
+ */
4
+ import { existsSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { $ } from 'bun';
7
+ import { METRO_PORT, DETOX_SERVER_PORT } from './constants';
8
+ /**
9
+ * Wait for Android device/emulator to be ready.
10
+ * Times out after 30 seconds if no device is available.
11
+ */
12
+ export async function waitForDevice() {
13
+ console.info('\n--- Waiting for device ---');
14
+ // Check if any device is connected first (with a quick timeout)
15
+ try {
16
+ const result = await $ `adb devices`.quiet();
17
+ const lines = result.stdout.toString().split('\n').filter(line => line.includes('\tdevice'));
18
+ if (lines.length === 0) {
19
+ throw new Error('No Android device/emulator connected. Please start an emulator first.');
20
+ }
21
+ }
22
+ catch (error) {
23
+ const err = error;
24
+ throw new Error(`No Android device available: ${err.message}`);
25
+ }
26
+ try {
27
+ // Wait for device to be fully booted (with timeout)
28
+ await $ `timeout 30 adb wait-for-device`.quiet();
29
+ await $ `timeout 60 adb shell 'while [ -z "$(getprop sys.boot_completed)" ]; do sleep 1; done'`.quiet();
30
+ console.info('Device is ready!');
31
+ }
32
+ catch (error) {
33
+ const err = error;
34
+ throw new Error(`Failed to wait for Android device: ${err.message}`);
35
+ }
36
+ }
37
+ /**
38
+ * Setup ADB reverse port forwarding for Metro and Detox server.
39
+ * This allows the emulator to connect to services on the host machine.
40
+ */
41
+ export async function setupAdbReverse() {
42
+ console.info('\n--- Setting up ADB reverse ---');
43
+ try {
44
+ // Metro bundler port
45
+ await $ `adb reverse tcp:${METRO_PORT} tcp:${METRO_PORT}`;
46
+ console.info(`Reversed port ${METRO_PORT} (Metro)`);
47
+ // Detox server port
48
+ await $ `adb reverse tcp:${DETOX_SERVER_PORT} tcp:${DETOX_SERVER_PORT}`;
49
+ console.info(`Reversed port ${DETOX_SERVER_PORT} (Detox)`);
50
+ }
51
+ catch (error) {
52
+ const err = error;
53
+ throw new Error(`Failed to setup ADB reverse ports: ${err.message}\n` +
54
+ 'Make sure the Android emulator is running and adb is available.');
55
+ }
56
+ }
57
+ /**
58
+ * Full Android device setup - wait for device and setup port forwarding.
59
+ */
60
+ export async function setupAndroidDevice() {
61
+ await waitForDevice();
62
+ await setupAdbReverse();
63
+ }
64
+ /**
65
+ * Ensure the android/ folder has full prebuild structure for Metro.
66
+ * In CI, the build job caches the entire android/ folder (including APKs and test files).
67
+ * The test job restores this cache and should NOT regenerate it.
68
+ *
69
+ * Why we DON'T always regenerate (unlike a previous approach):
70
+ * - The cached android/ folder includes DetoxTest.java and other manually-added test files
71
+ * - Running `expo prebuild --clean` would DELETE these critical test infrastructure files
72
+ * - The cached folder's fingerprint already ensures it's in sync with node_modules
73
+ *
74
+ * We check for android/build.gradle as the indicator of a complete prebuild.
75
+ * Only regenerate if the folder is missing or incomplete.
76
+ */
77
+ export async function ensureAndroidFolder() {
78
+ const buildGradlePath = join(process.cwd(), 'android', 'build.gradle');
79
+ if (existsSync(buildGradlePath)) {
80
+ console.info('Android folder already exists (build.gradle found)');
81
+ return;
82
+ }
83
+ console.info('\n--- Generating android/ folder (for Metro) ---');
84
+ console.info('Note: android/build.gradle not found, running expo prebuild');
85
+ try {
86
+ await $ `npx expo prebuild --platform android`;
87
+ console.info('Android folder generated!');
88
+ }
89
+ catch (error) {
90
+ const err = error;
91
+ throw new Error(`Failed to generate android folder: ${err.message}`);
92
+ }
93
+ }
package/dist/cli.mjs CHANGED
@@ -5,8 +5,8 @@ import { setGitHubOutput, isGitHubActions, isCI } from "./runner.mjs";
5
5
  import { ensureIosDeps, ensureAndroidDeps, ensureMaestro, printDepsStatus } from "./deps.mjs";
6
6
  import { withMetro } from "./metro.mjs";
7
7
  import { runDetoxTests } from "./detox.mjs";
8
- import { ensureIOSFolder, ensureIOSApp } from "./ios";
9
- import { setupAndroidDevice, ensureAndroidFolder } from "./android";
8
+ import { ensureIOSFolder, ensureIOSApp } from "./ios.mjs";
9
+ import { setupAndroidDevice, ensureAndroidFolder } from "./android.mjs";
10
10
  const HELP = `
11
11
  native-ci - Native CI/CD helpers for Expo apps
12
12
 
package/dist/index.mjs CHANGED
@@ -4,8 +4,8 @@ import { createCacheKey, saveFingerprintToKV, getFingerprintFromKV, extendKVTTL,
4
4
  import { runWithCache, setGitHubOutput, isGitHubActions, isCI } from "./runner.mjs";
5
5
  import { waitForMetro, prewarmBundle, startMetro, setupSignalHandlers, withMetro } from "./metro.mjs";
6
6
  import { parseDetoxArgs, buildDetoxArgs, runDetoxTests } from "./detox.mjs";
7
- import { waitForDevice, setupAdbReverse, setupAndroidDevice, ensureAndroidFolder } from "./android";
8
- import { ensureIOSFolder } from "./ios";
7
+ import { waitForDevice, setupAdbReverse, setupAndroidDevice, ensureAndroidFolder } from "./android.mjs";
8
+ import { ensureIOSFolder } from "./ios.mjs";
9
9
  import { checkDeps, ensureIosDeps, ensureAndroidDeps, ensureMaestro, printDepsStatus } from "./deps.mjs";
10
10
  export { DEFAULT_KV_TTL_SECONDS, DEFAULT_METRO_TIMEOUT_MS, DEFAULT_METRO_WAIT_ATTEMPTS, DEFAULT_METRO_WAIT_INTERVAL_MS, DETOX_SERVER_PORT, METRO_HOST, METRO_PORT, METRO_URL, buildDetoxArgs, checkDeps, createCacheKey, ensureAndroidDeps, ensureAndroidFolder, ensureIOSFolder, ensureIosDeps, ensureMaestro, extendKVTTL, generateFingerprint, generatePreFingerprintHash, getFingerprintFromKV, isCI, isGitHubActions, loadCache, parseDetoxArgs, prewarmBundle, printDepsStatus, runDetoxTests, runWithCache, saveCache, saveFingerprintToKV, setGitHubOutput, setupAdbReverse, setupAndroidDevice, setupSignalHandlers, startMetro, waitForDevice, waitForMetro, withMetro };
11
11
  //# sourceMappingURL=index.mjs.map
package/dist/ios.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * iOS-specific utilities for Detox test runners
3
+ */
4
+ import { existsSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { $ } from 'bun';
7
+ import { isCI } from './runner';
8
+ /**
9
+ * Ensure the ios/ folder has full prebuild structure for Metro.
10
+ * In CI, the build job may only cache the .app file, so the test job needs
11
+ * to regenerate the ios folder structure for Metro to work correctly.
12
+ *
13
+ * Metro needs the native project files (Podfile, *.xcodeproj) to properly
14
+ * configure the JS bundle with native module information (global.expo.modules).
15
+ *
16
+ * We check for ios/Podfile as the indicator of a complete prebuild,
17
+ * not just the existence of the ios/ folder.
18
+ *
19
+ * Uses --no-install to skip pod install (pods are not needed for Metro).
20
+ */
21
+ export async function ensureIOSFolder() {
22
+ const podfilePath = join(process.cwd(), 'ios', 'Podfile');
23
+ if (!existsSync(podfilePath)) {
24
+ console.info('\n--- Generating ios/ folder (for Metro) ---');
25
+ console.info('Note: ios/Podfile not found, running expo prebuild');
26
+ try {
27
+ await $ `npx expo prebuild --platform ios --no-install`;
28
+ console.info('iOS folder generated!');
29
+ }
30
+ catch (error) {
31
+ const err = error;
32
+ throw new Error(`Failed to generate ios folder: ${err.message}`);
33
+ }
34
+ }
35
+ else {
36
+ console.info('iOS folder already exists (Podfile found)');
37
+ }
38
+ }
39
+ /**
40
+ * Ensure the iOS app binary exists, building it if necessary.
41
+ * On CI, this is a no-op since CI builds the app in a separate job.
42
+ * Locally, this will build the app if the binary is missing.
43
+ */
44
+ export async function ensureIOSApp(config = 'ios.sim.debug') {
45
+ // On CI, the app is built separately - don't build here
46
+ if (isCI()) {
47
+ console.info('CI detected - skipping local build check');
48
+ return;
49
+ }
50
+ // Check if app binary exists (use the path from detoxrc)
51
+ const appPath = process.env.DETOX_IOS_APP_PATH ||
52
+ 'ios/build/Build/Products/Debug-iphonesimulator/tamaguikitchensink.app';
53
+ const fullAppPath = join(process.cwd(), appPath);
54
+ if (existsSync(fullAppPath)) {
55
+ console.info(`iOS app found at ${appPath}`);
56
+ return;
57
+ }
58
+ console.info(`\n--- iOS app not found at ${appPath}, building... ---`);
59
+ // Ensure pods are installed first
60
+ const podsPath = join(process.cwd(), 'ios', 'Pods');
61
+ if (!existsSync(podsPath)) {
62
+ console.info('Installing CocoaPods dependencies...');
63
+ await $ `pod install --project-directory=ios`;
64
+ }
65
+ // Build the app using detox build
66
+ console.info(`Building iOS app (config: ${config})...`);
67
+ console.info('This may take a few minutes on first run.');
68
+ await $ `npx detox build -c ${config}`;
69
+ console.info('iOS app built successfully!');
70
+ }
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Run Android Detox tests with Metro bundler
4
+ *
5
+ * Usage: bun run-detox-android.ts [options]
6
+ *
7
+ * Options:
8
+ * --config <name> Detox configuration name (default: android.emu.ci.debug)
9
+ * --project-root <path> Project root directory (default: cwd)
10
+ * --headless Run in headless mode
11
+ * --record-logs <mode> Record logs: none, failing, all (default: all)
12
+ * --retries <n> Number of retries for flaky tests (default: 0)
13
+ */
14
+ import { withMetro } from './metro';
15
+ import { parseDetoxArgs, runDetoxTests } from './detox';
16
+ import { waitForDevice, setupAdbReverse, ensureAndroidFolder } from './android';
17
+ const options = parseDetoxArgs('android');
18
+ console.info('=== Android Detox Test Runner ===');
19
+ console.info(`Config: ${options.config}`);
20
+ console.info(`Project root: ${options.projectRoot}`);
21
+ console.info(`Headless: ${options.headless}`);
22
+ // Change to project root
23
+ process.chdir(options.projectRoot);
24
+ // Ensure android folder exists (CI only caches APKs, not the full project)
25
+ await ensureAndroidFolder();
26
+ // Wait for Android device to be ready
27
+ await waitForDevice();
28
+ // Setup ADB reverse for Metro (Detox handles its own port via reversePorts config, but we need Metro early)
29
+ await setupAdbReverse();
30
+ // Run tests with Metro
31
+ const exitCode = await withMetro('android', async () => {
32
+ return runDetoxTests({
33
+ config: options.config,
34
+ projectRoot: options.projectRoot,
35
+ recordLogs: options.recordLogs,
36
+ retries: options.retries,
37
+ headless: options.headless,
38
+ });
39
+ });
40
+ process.exit(exitCode);
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Run iOS Detox tests with Metro bundler
4
+ *
5
+ * Usage: bun run-detox-ios.ts [options]
6
+ *
7
+ * Options:
8
+ * --config <name> Detox configuration name (default: ios.sim.debug)
9
+ * --project-root <path> Project root directory (default: cwd)
10
+ * --record-logs <mode> Record logs: none, failing, all (default: all)
11
+ * --retries <n> Number of retries for flaky tests (default: 0)
12
+ */
13
+ import { withMetro } from './metro';
14
+ import { parseDetoxArgs, runDetoxTests } from './detox';
15
+ import { ensureIOSFolder, ensureIOSApp } from './ios';
16
+ const options = parseDetoxArgs('ios');
17
+ console.info('=== iOS Detox Test Runner ===');
18
+ console.info(`Config: ${options.config}`);
19
+ console.info(`Project root: ${options.projectRoot}`);
20
+ // Change to project root
21
+ process.chdir(options.projectRoot);
22
+ // Ensure ios folder exists (in case build artifacts were separated)
23
+ await ensureIOSFolder();
24
+ // Ensure iOS app is built (skipped on CI where app is pre-built)
25
+ await ensureIOSApp(options.config);
26
+ // Run tests with Metro
27
+ const exitCode = await withMetro('ios', async () => {
28
+ return runDetoxTests({
29
+ config: options.config,
30
+ projectRoot: options.projectRoot,
31
+ recordLogs: options.recordLogs,
32
+ retries: options.retries,
33
+ });
34
+ });
35
+ process.exit(exitCode);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamagui/native-ci",
3
- "version": "1.144.1",
3
+ "version": "1.144.3",
4
4
  "description": "Native CI/CD helpers for React Native apps with Expo - fingerprinting, caching, and build optimization",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,7 +39,7 @@
39
39
  "@expo/fingerprint": "^0.15.3"
40
40
  },
41
41
  "devDependencies": {
42
- "@tamagui/build": "1.144.1",
42
+ "@tamagui/build": "1.144.3",
43
43
  "@types/bun": "^1.1.0",
44
44
  "@types/node": "^22.1.0"
45
45
  },