Skip to content

Commit fd1b41b

Browse files
feat: add Bun to init command (#2073)
* feat: support bun package manager * rename function & fix tests * check if bun is available * create --pm flag and deprecate --npm * prioritize flag over user agent * update checkPackageManagerAvailability fn * set yarn as default PM if user agent not available * add tests for bun
1 parent 1ba2916 commit fd1b41b

File tree

14 files changed

+346
-629
lines changed

14 files changed

+346
-629
lines changed

docs/commands.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,15 @@ module.exports = {
9797
Skip dependencies installation
9898

9999
#### `--npm`
100+
> [!WARNING]
101+
> `--npm` is deprecated and will be removed in the future. Please use `--pm npm` instead.
100102
101103
Force use of npm during initialization
102104

105+
#### `--pm <string>`
106+
107+
Use specific package manager to initialize the project. Available options: `yarn`, `npm`, `bun`. Default: `yarn`
108+
103109
#### `--package-name <string>`
104110

105111
Create project with custom package name for Android and bundle identifier for iOS. The correct package name should:

packages/cli-clean/src/clean.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,17 @@ export async function clean(
163163
: []),
164164
],
165165
},
166+
bun: {
167+
description: 'Bun cache',
168+
tasks: [
169+
{
170+
label: 'Clean Bun cache',
171+
action: async () => {
172+
await execa('bun', ['pm', 'cache', 'rm'], {cwd: projectRoot});
173+
},
174+
},
175+
],
176+
},
166177
watchman: {
167178
description: 'Stop Watchman and delete its cache',
168179
tasks: [

packages/cli-doctor/src/tools/checkInstallation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import commandExists from 'command-exists';
44
export enum PACKAGE_MANAGERS {
55
YARN = 'YARN',
66
NPM = 'NPM',
7+
BUN = 'BUN',
78
}
89

910
const isSoftwareNotInstalled = async (command: string): Promise<boolean> => {

packages/cli-doctor/src/tools/versionRanges.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export default {
33
NODE_JS: '>= 18',
44
YARN: '>= 1.10.x',
55
NPM: '>= 4.x',
6+
BUN: '>= 1.0.0',
67
RUBY: '>= 2.6.10',
78
JAVA: '>= 17 <= 20',
89
// Android

packages/cli-doctor/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type EnvironmentInfo = {
2323
Node: AvailableInformation;
2424
Yarn: AvailableInformation;
2525
npm: AvailableInformation;
26+
bun: AvailableInformation;
2627
Watchman: AvailableInformation;
2728
};
2829
Managers: {

packages/cli/src/commands/init/__tests__/template.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ afterEach(() => {
2222
test('installTemplatePackage', async () => {
2323
jest.spyOn(PackageManger, 'install').mockImplementationOnce(() => null);
2424

25-
await installTemplatePackage(TEMPLATE_NAME, TEMPLATE_SOURCE_DIR, true);
25+
await installTemplatePackage(TEMPLATE_NAME, TEMPLATE_SOURCE_DIR, 'npm');
2626

2727
expect(PackageManger.install).toHaveBeenCalledWith([TEMPLATE_NAME], {
28-
preferYarn: false,
28+
packageManager: 'npm',
2929
silent: true,
3030
root: TEMPLATE_SOURCE_DIR,
3131
});

packages/cli/src/commands/init/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export default {
2020
name: '--npm',
2121
description: 'Forces using npm for initialization',
2222
},
23+
{
24+
name: '--pm <string>',
25+
description:
26+
'Use specific package manager to initialize the project. Available options: `yarn`, `npm`, `bun`. Default: `yarn`',
27+
},
2328
{
2429
name: '--directory <string>',
2530
description: 'Uses a custom directory instead of `<projectName>`.',

packages/cli/src/commands/init/init.ts

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ import * as PackageManager from '../../tools/packageManager';
2121
import {installPods} from '@react-native-community/cli-doctor';
2222
import banner from './banner';
2323
import TemplateAndVersionError from './errors/TemplateAndVersionError';
24+
import {getBunVersionIfAvailable} from '../../tools/bun';
25+
import {getNpmVersionIfAvailable} from '../../tools/npm';
26+
import {getYarnVersionIfAvailable} from '../../tools/yarn';
2427

2528
const DEFAULT_VERSION = 'latest';
2629

2730
type Options = {
2831
template?: string;
2932
npm?: boolean;
33+
pm?: PackageManager.PackageManager;
3034
directory?: string;
3135
displayName?: string;
3236
title?: string;
@@ -39,6 +43,7 @@ interface TemplateOptions {
3943
projectName: string;
4044
templateUri: string;
4145
npm?: boolean;
46+
pm?: PackageManager.PackageManager;
4247
directory: string;
4348
projectTitle?: string;
4449
skipInstall?: boolean;
@@ -82,6 +87,7 @@ async function createFromTemplate({
8287
projectName,
8388
templateUri,
8489
npm,
90+
pm,
8591
directory,
8692
projectTitle,
8793
skipInstall,
@@ -90,6 +96,24 @@ async function createFromTemplate({
9096
logger.debug('Initializing new project');
9197
logger.log(banner);
9298

99+
let packageManager = pm;
100+
101+
if (pm) {
102+
packageManager = pm;
103+
} else {
104+
const userAgentPM = userAgentPackageManager();
105+
// if possible, use the package manager from the user agent. Otherwise fallback to default (yarn)
106+
packageManager = userAgentPM || 'yarn';
107+
}
108+
109+
if (npm) {
110+
logger.warn(
111+
'Flag --npm is deprecated and will be removed soon. In the future, please use --pm npm instead.',
112+
);
113+
114+
packageManager = 'npm';
115+
}
116+
93117
const projectDirectory = await setProjectDirectory(directory);
94118

95119
const loader = getLoader({text: 'Downloading template'});
@@ -100,7 +124,11 @@ async function createFromTemplate({
100124
try {
101125
loader.start();
102126

103-
await installTemplatePackage(templateUri, templateSourceDir, npm);
127+
await installTemplatePackage(
128+
templateUri,
129+
templateSourceDir,
130+
packageManager,
131+
);
104132

105133
loader.succeed();
106134
loader.start('Copying template');
@@ -137,7 +165,7 @@ async function createFromTemplate({
137165

138166
if (!skipInstall) {
139167
await installDependencies({
140-
npm,
168+
packageManager,
141169
loader,
142170
root: projectDirectory,
143171
});
@@ -153,18 +181,18 @@ async function createFromTemplate({
153181
}
154182

155183
async function installDependencies({
156-
npm,
184+
packageManager,
157185
loader,
158186
root,
159187
}: {
160-
npm?: boolean;
188+
packageManager: PackageManager.PackageManager;
161189
loader: Loader;
162190
root: string;
163191
}) {
164192
loader.start('Installing dependencies');
165193

166194
await PackageManager.installAll({
167-
preferYarn: !npm,
195+
packageManager,
168196
silent: true,
169197
root,
170198
});
@@ -176,6 +204,20 @@ async function installDependencies({
176204
loader.succeed();
177205
}
178206

207+
function checkPackageManagerAvailability(
208+
packageManager: PackageManager.PackageManager,
209+
) {
210+
if (packageManager === 'bun') {
211+
return getBunVersionIfAvailable();
212+
} else if (packageManager === 'npm') {
213+
return getNpmVersionIfAvailable();
214+
} else if (packageManager === 'yarn') {
215+
return getYarnVersionIfAvailable();
216+
}
217+
218+
return false;
219+
}
220+
179221
function createTemplateUri(options: Options, version: string): string {
180222
const isTypescriptTemplate =
181223
options.template === 'react-native-template-typescript';
@@ -202,13 +244,32 @@ async function createProject(
202244
projectName,
203245
templateUri,
204246
npm: options.npm,
247+
pm: options.pm,
205248
directory,
206249
projectTitle: options.title,
207250
skipInstall: options.skipInstall,
208251
packageName: options.packageName,
209252
});
210253
}
211254

255+
function userAgentPackageManager() {
256+
const userAgent = process.env.npm_config_user_agent;
257+
258+
if (userAgent) {
259+
if (userAgent.startsWith('yarn')) {
260+
return 'yarn';
261+
}
262+
if (userAgent.startsWith('npm')) {
263+
return 'npm';
264+
}
265+
if (userAgent.startsWith('bun')) {
266+
return 'bun';
267+
}
268+
}
269+
270+
return null;
271+
}
272+
212273
export default (async function initialize(
213274
[projectName]: Array<string>,
214275
options: Options,
@@ -223,6 +284,13 @@ export default (async function initialize(
223284
const version = options.version || DEFAULT_VERSION;
224285
const directoryName = path.relative(root, options.directory || projectName);
225286

287+
if (options.pm && !checkPackageManagerAvailability(options.pm)) {
288+
logger.error(
289+
'Seems like the package manager you want to use is not installed. Please install it or choose another package manager.',
290+
);
291+
return;
292+
}
293+
226294
await createProject(projectName, directoryName, version, options);
227295

228296
const projectFolder = path.join(root, directoryName);

packages/cli/src/commands/init/template.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@ export type TemplateConfig = {
1717
export async function installTemplatePackage(
1818
templateName: string,
1919
root: string,
20-
npm?: boolean,
20+
packageManager: PackageManager.PackageManager,
2121
) {
2222
logger.debug(`Installing template from ${templateName}`);
2323

2424
await PackageManager.init({
25-
preferYarn: !npm,
25+
packageManager,
2626
silent: true,
2727
root,
2828
});
2929

3030
return PackageManager.install([templateName], {
31-
preferYarn: !npm,
31+
packageManager,
3232
silent: true,
3333
root,
3434
});

0 commit comments

Comments
 (0)