Skip to content

Commit 817bcd7

Browse files
authored
Merge pull request #67 from 10play/17Amir17/fix-non-npm-init
feat(cli): support yarn, pnpm, and bun package managers
2 parents a711eca + c390c51 commit 817bcd7

File tree

7 files changed

+107
-31
lines changed

7 files changed

+107
-31
lines changed

cli/commands/dev.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { spawn, execSync, type ChildProcess } from "child_process";
33
import { existsSync, unlinkSync } from "fs";
44
import { join } from "path";
55
import { DevEnvironment, killProcessTree } from "../runner/devEnvironment.js";
6-
import { writeLocalConfig, updateInfoPlist, updateAndroidManifest, getPackageRoot, appendSecret, resolveAndroidJavaHome } from "../utils/common.js";
6+
import { writeLocalConfig, updateInfoPlist, updateAndroidManifest, getPackageRoot, appendSecret, resolveAndroidJavaHome, detectPackageManager, getExecCommand } from "../utils/common.js";
77

88
export interface DevOptions {
99
port: string;
@@ -26,7 +26,7 @@ export async function devCommand(options: DevOptions): Promise<void> {
2626
tunnel: false,
2727
server: true,
2828
runWidgetMetro: true,
29-
metroCommand: "npm",
29+
metroCommand: "run-script",
3030
watchServer: true,
3131
});
3232

@@ -174,9 +174,12 @@ export async function devCommand(options: DevOptions): Promise<void> {
174174
}
175175
}
176176

177+
const pm = detectPackageManager(projectRoot);
178+
const exec = getExecCommand(pm);
179+
177180
buildProcess = spawn(
178-
"npx",
179-
runArgs,
181+
exec.cmd,
182+
[...exec.args, ...runArgs],
180183
{
181184
cwd: projectRoot,
182185
stdio: "inherit",

cli/commands/fly.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import chalk from "chalk";
22
import { spawn } from "child_process";
33
import { DevEnvironment } from "../runner/devEnvironment.js";
44
import { detectAllDevices, selectDevice, ConnectedDevice } from "../utils/devices.js";
5-
import { getGitBranchSuffix, maskSecret, resolveAndroidJavaHome } from "../utils/common.js";
5+
import { getGitBranchSuffix, maskSecret, resolveAndroidJavaHome, detectPackageManager, getExecCommand } from "../utils/common.js";
66

77
export interface FlyOptions {
88
port: string;
@@ -59,7 +59,7 @@ export async function flyCommand(options: FlyOptions): Promise<void> {
5959
tunnel: options.tunnel,
6060
server: true, // fly always has server
6161
runWidgetMetro: options.dev ?? false, // Only in dev mode
62-
metroCommand: "npx", // fly uses npx expo start
62+
metroCommand: "exec", // fly uses <pm-exec> expo start
6363
watchServer: options.dev ?? false, // Watch prompt server in dev mode
6464
});
6565

@@ -167,7 +167,10 @@ export async function flyCommand(options: FlyOptions): Promise<void> {
167167
String(ports.appMetro),
168168
];
169169

170-
const buildProcess = spawn("npx", buildArgs, {
170+
const pm = detectPackageManager(projectRoot);
171+
const exec = getExecCommand(pm);
172+
173+
const buildProcess = spawn(exec.cmd, [...exec.args, ...buildArgs], {
171174
cwd: projectRoot,
172175
stdio: "inherit",
173176
env: buildEnv,

cli/commands/init.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import chalk from "chalk";
22
import { execSync, spawn } from "child_process";
33
import * as path from "path";
44
import * as fs from "fs";
5+
import { detectPackageManager, getInstallCommand, getExecCommand } from "../utils/common.js";
56
interface InitOptions {
67
force?: boolean;
78
skipPrebuild?: boolean;
@@ -50,17 +51,11 @@ export async function initCommand(options: InitOptions): Promise<void> {
5051
path.join(projectRoot, "node_modules", "@10play", "expo-air"),
5152
);
5253

54+
const pm = detectPackageManager(projectRoot);
55+
5356
if (!allDeps["@10play/expo-air"] || !moduleExists) {
5457
console.log(chalk.gray(" Installing @10play/expo-air...\n"));
55-
const cmd =
56-
fs.existsSync(path.join(projectRoot, "bun.lockb")) ||
57-
fs.existsSync(path.join(projectRoot, "bun.lock"))
58-
? "bun add @10play/expo-air"
59-
: fs.existsSync(path.join(projectRoot, "pnpm-lock.yaml"))
60-
? "pnpm add @10play/expo-air"
61-
: fs.existsSync(path.join(projectRoot, "yarn.lock"))
62-
? "yarn add @10play/expo-air"
63-
: "npm install @10play/expo-air";
58+
const cmd = getInstallCommand(pm, "@10play/expo-air");
6459

6560
try {
6661
execSync(cmd, { cwd: projectRoot, stdio: "inherit" });
@@ -158,9 +153,12 @@ export async function initCommand(options: InitOptions): Promise<void> {
158153
),
159154
);
160155

156+
const exec = getExecCommand(pm);
157+
const prebuildCmd = [...exec.args, "expo", "prebuild", "--clean"];
158+
161159
try {
162160
await new Promise<void>((resolve, reject) => {
163-
const prebuild = spawn("npx", ["expo", "prebuild", "--clean"], {
161+
const prebuild = spawn(exec.cmd, prebuildCmd, {
164162
cwd: projectRoot,
165163
stdio: "inherit",
166164
shell: true,
@@ -184,21 +182,23 @@ export async function initCommand(options: InitOptions): Promise<void> {
184182
const message = err instanceof Error ? err.message : String(err);
185183
console.log(chalk.red(`\n Prebuild failed: ${message}`));
186184
console.log(
187-
chalk.gray(" You can run it manually: npx expo prebuild --clean\n"),
185+
chalk.gray(` You can run it manually: ${exec.cmd} ${prebuildCmd.join(" ")}\n`),
188186
);
189187
process.exit(1);
190188
}
191189
} else {
190+
const exec = getExecCommand(pm);
192191
console.log(chalk.yellow("\n Skipped prebuild (--skip-prebuild)"));
193-
console.log(chalk.gray(" Run manually: npx expo prebuild --clean\n"));
192+
console.log(chalk.gray(` Run manually: ${exec.cmd} ${[...exec.args, "expo", "prebuild", "--clean"].join(" ")}\n`));
194193
}
195194

196195
// Success message
196+
const exec = getExecCommand(pm);
197197
console.log(chalk.blue("\n expo-air initialized!\n"));
198198
console.log(chalk.gray(" Next steps:"));
199199
console.log(
200200
chalk.white(" 1. Connect your iOS or Android device via cable"),
201201
);
202-
console.log(chalk.white(" 2. Run: npx expo-air fly"));
202+
console.log(chalk.white(` 2. Run: ${exec.cmd} ${[...exec.args, "expo-air", "fly"].join(" ")}`));
203203
console.log(chalk.white(" 3. The widget will appear on your device\n"));
204204
}

cli/commands/start.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export async function startCommand(options: StartOptions): Promise<void> {
2424
tunnel: options.tunnel,
2525
server: options.server,
2626
// runWidgetMetro: auto-detect (default behavior)
27-
metroCommand: "npm", // start uses npm start
27+
metroCommand: "run-script", // start uses the project's start script
2828
});
2929

3030
// Allocate ports

cli/runner/devEnvironment.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ export interface DevEnvironmentOptions {
5757
tunnel?: boolean;
5858
/** Enable prompt server (default: true) */
5959
server?: boolean;
60-
/** Run widget Metro server (default: auto-detect based on npm install) */
60+
/** Run widget Metro server (default: auto-detect based on install) */
6161
runWidgetMetro?: boolean;
62-
/** Metro command to use: 'npm' or 'npx' (default: 'npm') */
62+
/** Metro command to use: 'run-script' or 'exec' (default: 'run-script') */
6363
metroCommand?: MetroCommand;
6464
/** Watch prompt server files and restart on changes (default: false) */
6565
watchServer?: boolean;
@@ -130,7 +130,7 @@ export class DevEnvironment {
130130
tunnel: options.tunnel ?? true,
131131
server: options.server ?? true,
132132
runWidgetMetro: shouldRunWidgetMetro,
133-
metroCommand: options.metroCommand ?? "npm",
133+
metroCommand: options.metroCommand ?? "run-script",
134134
watchServer: options.watchServer ?? false,
135135
} as Required<DevEnvironmentOptions>;
136136

cli/utils/common.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,72 @@ export function resolveAndroidJavaHome(): string | null {
434434
return null;
435435
}
436436

437+
export type PackageManager = "npm" | "yarn" | "pnpm" | "bun";
438+
439+
/**
440+
* Detect the package manager used in a project by checking lock files.
441+
*/
442+
export function detectPackageManager(projectRoot: string): PackageManager {
443+
if (
444+
fs.existsSync(path.join(projectRoot, "bun.lockb")) ||
445+
fs.existsSync(path.join(projectRoot, "bun.lock"))
446+
) {
447+
return "bun";
448+
}
449+
if (fs.existsSync(path.join(projectRoot, "pnpm-lock.yaml"))) {
450+
return "pnpm";
451+
}
452+
if (fs.existsSync(path.join(projectRoot, "yarn.lock"))) {
453+
return "yarn";
454+
}
455+
return "npm";
456+
}
457+
458+
/**
459+
* Get the command and args prefix for executing a local package binary.
460+
* Equivalent of `npx` for each package manager.
461+
*
462+
* Usage: spawn(exec.cmd, [...exec.args, "expo", "prebuild", "--clean"])
463+
*/
464+
export function getExecCommand(pm: PackageManager): { cmd: string; args: string[] } {
465+
switch (pm) {
466+
case "bun": return { cmd: "bunx", args: [] };
467+
case "pnpm": return { cmd: "pnpm", args: ["exec"] };
468+
case "yarn": return { cmd: "yarn", args: [] };
469+
case "npm": return { cmd: "npx", args: [] };
470+
}
471+
}
472+
473+
/**
474+
* Get the full install command string for a package.
475+
*/
476+
export function getInstallCommand(pm: PackageManager, pkg: string): string {
477+
switch (pm) {
478+
case "bun": return `bun add ${pkg}`;
479+
case "pnpm": return `pnpm add ${pkg}`;
480+
case "yarn": return `yarn add ${pkg}`;
481+
case "npm": return `npm install ${pkg}`;
482+
}
483+
}
484+
485+
/**
486+
* Get command + args for running a package.json script with extra args.
487+
*
488+
* Usage: spawn(run.cmd, run.args)
489+
*/
490+
export function getRunScriptCommand(
491+
pm: PackageManager,
492+
script: string,
493+
extraArgs: string[]
494+
): { cmd: string; args: string[] } {
495+
switch (pm) {
496+
case "npm": return { cmd: "npm", args: [script, "--", ...extraArgs] };
497+
case "yarn": return { cmd: "yarn", args: [script, ...extraArgs] };
498+
case "pnpm": return { cmd: "pnpm", args: [script, ...extraArgs] };
499+
case "bun": return { cmd: "bun", args: ["run", script, ...extraArgs] };
500+
}
501+
}
502+
437503
export function getGitBranchSuffix(cwd?: string): string | null {
438504
try {
439505
const branch = execSync("git rev-parse --abbrev-ref HEAD", {

cli/utils/metro.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import chalk from "chalk";
22
import { spawn, ChildProcess } from "child_process";
3-
import { waitForPort } from "./common.js";
3+
import { waitForPort, detectPackageManager, getExecCommand, getRunScriptCommand } from "./common.js";
44

55
export interface MetroProcess {
66
process: ChildProcess;
77
port: number;
88
name: string;
99
}
1010

11-
export type MetroCommand = "npm" | "npx";
11+
export type MetroCommand = "run-script" | "exec";
1212

1313
export interface StartMetroOptions {
1414
name: string;
1515
cwd: string;
1616
port: number;
17-
/** Use 'npm' for `npm start -- --port`, 'npx' for `npx expo start --port` */
17+
/** Use 'run-script' for `<pm> start --port`, 'exec' for `<pm-exec> expo start --port` */
1818
command?: MetroCommand;
1919
/** Timeout for waiting for port to be ready (default: 30000ms) */
2020
timeout?: number;
@@ -24,19 +24,23 @@ export interface StartMetroOptions {
2424
* Start a Metro bundler server
2525
*/
2626
export async function startMetro(options: StartMetroOptions): Promise<ChildProcess | null> {
27-
const { name, cwd, port, command = "npm", timeout = 30000 } = options;
27+
const { name, cwd, port, command = "run-script", timeout = 30000 } = options;
28+
29+
const pm = detectPackageManager(cwd);
2830

2931
try {
3032
let proc: ChildProcess;
3133

32-
if (command === "npm") {
33-
proc = spawn("npm", ["start", "--", "--port", String(port)], {
34+
if (command === "run-script") {
35+
const run = getRunScriptCommand(pm, "start", ["--port", String(port)]);
36+
proc = spawn(run.cmd, run.args, {
3437
cwd,
3538
stdio: ["ignore", "pipe", "pipe"],
3639
env: { ...process.env, FORCE_COLOR: "1" },
3740
});
3841
} else {
39-
proc = spawn("npx", ["expo", "start", "--port", String(port)], {
42+
const exec = getExecCommand(pm);
43+
proc = spawn(exec.cmd, [...exec.args, "expo", "start", "--port", String(port)], {
4044
cwd,
4145
stdio: ["ignore", "pipe", "pipe"],
4246
env: {

0 commit comments

Comments
 (0)