Skip to content

Commit f758f6c

Browse files
feat: add experimental bun package manager support (#5791)
Co-authored-by: Igor Randjelovic <rigor789@gmail.com>
1 parent 7c87b49 commit f758f6c

11 files changed

+533
-270
lines changed

lib/bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ injector.requirePublic("npm", "./node-package-manager");
225225
injector.requirePublic("yarn", "./yarn-package-manager");
226226
injector.requirePublic("yarn2", "./yarn2-package-manager");
227227
injector.requirePublic("pnpm", "./pnpm-package-manager");
228+
injector.requirePublic("bun", "./bun-package-manager");
228229
injector.requireCommand(
229230
"package-manager|*get",
230231
"./commands/package-manager-get"

lib/bun-package-manager.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import * as path from "path";
2+
import { BasePackageManager } from "./base-package-manager";
3+
import { exported, cache } from "./common/decorators";
4+
import { CACACHE_DIRECTORY_NAME } from "./constants";
5+
import * as _ from "lodash";
6+
import {
7+
INodePackageManagerInstallOptions,
8+
INpmInstallResultInfo,
9+
INpmsResult,
10+
} from "./declarations";
11+
import {
12+
IChildProcess,
13+
IErrors,
14+
IFileSystem,
15+
IHostInfo,
16+
Server,
17+
} from "./common/declarations";
18+
import { injector } from "./common/yok";
19+
20+
export class BunPackageManager extends BasePackageManager {
21+
constructor(
22+
$childProcess: IChildProcess,
23+
private $errors: IErrors,
24+
$fs: IFileSystem,
25+
$hostInfo: IHostInfo,
26+
private $logger: ILogger,
27+
private $httpClient: Server.IHttpClient,
28+
$pacoteService: IPacoteService
29+
) {
30+
super($childProcess, $fs, $hostInfo, $pacoteService, "bun");
31+
}
32+
33+
@exported("bun")
34+
public async install(
35+
packageName: string,
36+
pathToSave: string,
37+
config: INodePackageManagerInstallOptions
38+
): Promise<INpmInstallResultInfo> {
39+
if (config.disableNpmInstall) {
40+
return;
41+
}
42+
if (config.ignoreScripts) {
43+
config["ignore-scripts"] = true;
44+
}
45+
46+
const packageJsonPath = path.join(pathToSave, "package.json");
47+
const jsonContentBefore = this.$fs.readJson(packageJsonPath);
48+
49+
const flags = this.getFlagsString(config, true);
50+
// TODO: Confirm desired behavior. The npm version uses --legacy-peer-deps
51+
// by default, we could use `--no-peer` for Bun if similar is needed; the
52+
// pnpm version uses `--shamefully-hoist`, but Bun has no similar flag.
53+
let params = ["install", "--legacy-peer-deps"];
54+
const isInstallingAllDependencies = packageName === pathToSave;
55+
if (!isInstallingAllDependencies) {
56+
params.push(packageName);
57+
}
58+
59+
params = params.concat(flags);
60+
const cwd = pathToSave;
61+
62+
try {
63+
const result = await this.processPackageManagerInstall(
64+
packageName,
65+
params,
66+
{ cwd, isInstallingAllDependencies }
67+
);
68+
return result;
69+
} catch (err) {
70+
// Revert package.json contents to preserve valid state
71+
this.$fs.writeJson(packageJsonPath, jsonContentBefore);
72+
throw err;
73+
}
74+
}
75+
76+
@exported("bun")
77+
public async uninstall(
78+
packageName: string,
79+
config?: any,
80+
cwd?: string
81+
): Promise<string> {
82+
const flags = this.getFlagsString(config, false);
83+
return this.$childProcess.exec(`bun remove ${packageName} ${flags}`, {
84+
cwd,
85+
});
86+
}
87+
88+
// Bun does not have a `view` command; use npm.
89+
@exported("bun")
90+
public async view(packageName: string, config: Object): Promise<any> {
91+
const wrappedConfig = _.extend({}, config, { json: true }); // always require view response as JSON
92+
93+
const flags = this.getFlagsString(wrappedConfig, false);
94+
let viewResult: any;
95+
try {
96+
viewResult = await this.$childProcess.exec(
97+
`npm view ${packageName} ${flags}`
98+
);
99+
} catch (e) {
100+
this.$errors.fail(e.message);
101+
}
102+
103+
try {
104+
return JSON.parse(viewResult);
105+
} catch (err) {
106+
return null;
107+
}
108+
}
109+
110+
// Bun does not have a `search` command; use npm.
111+
@exported("bun")
112+
public async search(filter: string[], config: any): Promise<string> {
113+
const flags = this.getFlagsString(config, false);
114+
return this.$childProcess.exec(`npm search ${filter.join(" ")} ${flags}`);
115+
}
116+
117+
public async searchNpms(keyword: string): Promise<INpmsResult> {
118+
// Bugs with npms.io:
119+
// 1. API returns no results when a valid package name contains @ or /
120+
// even if using encodeURIComponent().
121+
// 2. npms.io's API no longer returns updated results; see
122+
// https://github.com/npms-io/npms-api/issues/112. Better to switch to
123+
// https://registry.npmjs.org/<query>
124+
const httpRequestResult = await this.$httpClient.httpRequest(
125+
`https://api.npms.io/v2/search?q=keywords:${keyword}`
126+
);
127+
const result: INpmsResult = JSON.parse(httpRequestResult.body);
128+
return result;
129+
}
130+
131+
// Bun does not have a command analogous to `npm config get registry`; Bun
132+
// uses `bunfig.toml` to define custom registries.
133+
// - TODO: read `bunfig.toml`, if it exists, and return the registry URL.
134+
public async getRegistryPackageData(packageName: string): Promise<any> {
135+
const registry = await this.$childProcess.exec(`npm config get registry`);
136+
const url = registry.trim() + packageName;
137+
this.$logger.trace(
138+
`Trying to get data from npm registry for package ${packageName}, url is: ${url}`
139+
);
140+
const responseData = (await this.$httpClient.httpRequest(url)).body;
141+
this.$logger.trace(
142+
`Successfully received data from npm registry for package ${packageName}. Response data is: ${responseData}`
143+
);
144+
const jsonData = JSON.parse(responseData);
145+
this.$logger.trace(
146+
`Successfully parsed data from npm registry for package ${packageName}.`
147+
);
148+
return jsonData;
149+
}
150+
151+
@cache()
152+
public async getCachePath(): Promise<string> {
153+
const cachePath = await this.$childProcess.exec(`bun pm cache`);
154+
return path.join(cachePath.trim(), CACACHE_DIRECTORY_NAME);
155+
}
156+
}
157+
158+
injector.register("bun", BunPackageManager);

lib/commands/preview.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,24 @@ export class PreviewCommand implements ICommand {
4444
const previewCLIPath = this.getPreviewCLIPath();
4545

4646
if (!previewCLIPath) {
47-
const packageManagerName = await this.$packageManager.getPackageManagerName();
47+
const packageManagerName =
48+
await this.$packageManager.getPackageManagerName();
4849
let installCommand = "";
4950

5051
switch (packageManagerName) {
51-
case PackageManagers.npm:
52-
installCommand = "npm install --save-dev @nativescript/preview-cli";
53-
break;
5452
case PackageManagers.yarn:
5553
case PackageManagers.yarn2:
5654
installCommand = "yarn add -D @nativescript/preview-cli";
5755
break;
5856
case PackageManagers.pnpm:
5957
installCommand = "pnpm install --save-dev @nativescript/preview-cli";
6058
break;
59+
case PackageManagers.bun:
60+
installCommand = "bun add --dev @nativescript/preview-cli";
61+
case PackageManagers.npm:
62+
default:
63+
installCommand = "npm install --save-dev @nativescript/preview-cli";
64+
break;
6165
}
6266
this.$logger.info(
6367
[

lib/common/dispatchers.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,20 @@ export class CommandDispatcher implements ICommandDispatcher {
120120
let updateCommand = "";
121121

122122
switch (packageManagerName) {
123-
case PackageManagers.npm:
124-
updateCommand = "npm i -g nativescript";
125-
break;
126123
case PackageManagers.yarn:
127124
case PackageManagers.yarn2:
128125
updateCommand = "yarn global add nativescript";
129126
break;
130127
case PackageManagers.pnpm:
131128
updateCommand = "pnpm i -g nativescript";
132129
break;
130+
case PackageManagers.bun:
131+
updateCommand = "bun add --global nativescript";
132+
break;
133+
case PackageManagers.npm:
134+
default:
135+
updateCommand = "npm i -g nativescript";
136+
break;
133137
}
134138

135139
if (

lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,4 +492,5 @@ export enum PackageManagers {
492492
pnpm = "pnpm",
493493
yarn = "yarn",
494494
yarn2 = "yarn2",
495+
bun = "bun",
495496
}

lib/package-manager.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class PackageManager implements IPackageManager {
2828
private $yarn: INodePackageManager,
2929
private $yarn2: INodePackageManager,
3030
private $pnpm: INodePackageManager,
31+
private $bun: INodePackageManager,
3132
private $logger: ILogger,
3233
private $userSettingsService: IUserSettingsService,
3334
private $projectConfigService: IProjectConfigService
@@ -144,9 +145,8 @@ export class PackageManager implements IPackageManager {
144145
}
145146

146147
try {
147-
const configPm = this.$projectConfigService.getValue(
148-
"cli.packageManager"
149-
);
148+
const configPm =
149+
this.$projectConfigService.getValue("cli.packageManager");
150150

151151
if (configPm) {
152152
this.$logger.trace(
@@ -172,6 +172,9 @@ export class PackageManager implements IPackageManager {
172172
} else if (pm === PackageManagers.pnpm || this.$options.pnpm) {
173173
this._packageManagerName = PackageManagers.pnpm;
174174
return this.$pnpm;
175+
} else if (pm === PackageManagers.bun) {
176+
this._packageManagerName = PackageManagers.bun;
177+
return this.$bun;
175178
} else {
176179
this._packageManagerName = PackageManagers.npm;
177180
return this.$npm;

test/bun-package-manager.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Yok } from "../lib/common/yok";
2+
import * as stubs from "./stubs";
3+
import { assert } from "chai";
4+
import { BunPackageManager } from "../lib/bun-package-manager";
5+
import { IInjector } from "../lib/common/definitions/yok";
6+
7+
function createTestInjector(configuration: {} = {}): IInjector {
8+
const injector = new Yok();
9+
injector.register("hostInfo", {});
10+
injector.register("errors", stubs.ErrorsStub);
11+
injector.register("logger", stubs.LoggerStub);
12+
injector.register("childProcess", stubs.ChildProcessStub);
13+
injector.register("httpClient", {});
14+
injector.register("fs", stubs.FileSystemStub);
15+
injector.register("bun", BunPackageManager);
16+
injector.register("pacoteService", {
17+
manifest: () => Promise.resolve(),
18+
});
19+
20+
return injector;
21+
}
22+
23+
describe("node-package-manager", () => {
24+
describe("getPackageNameParts", () => {
25+
[
26+
{
27+
name: "should return both name and version when valid fullName passed",
28+
templateFullName: "some-template@1.0.0",
29+
expectedVersion: "1.0.0",
30+
expectedName: "some-template",
31+
},
32+
{
33+
name: "should return both name and version when valid fullName with scope passed",
34+
templateFullName: "@nativescript/some-template@1.0.0",
35+
expectedVersion: "1.0.0",
36+
expectedName: "@nativescript/some-template",
37+
},
38+
{
39+
name: "should return only name when version is not specified and the template is scoped",
40+
templateFullName: "@nativescript/some-template",
41+
expectedVersion: "",
42+
expectedName: "@nativescript/some-template",
43+
},
44+
{
45+
name: "should return only name when version is not specified",
46+
templateFullName: "some-template",
47+
expectedVersion: "",
48+
expectedName: "some-template",
49+
},
50+
].forEach((testCase) => {
51+
it(testCase.name, async () => {
52+
const testInjector = createTestInjector();
53+
const npm = testInjector.resolve<BunPackageManager>("bun");
54+
const templateNameParts = await npm.getPackageNameParts(
55+
testCase.templateFullName
56+
);
57+
assert.strictEqual(templateNameParts.name, testCase.expectedName);
58+
assert.strictEqual(templateNameParts.version, testCase.expectedVersion);
59+
});
60+
});
61+
});
62+
63+
describe("getPackageFullName", () => {
64+
[
65+
{
66+
name: "should return name and version when specified",
67+
templateName: "some-template",
68+
templateVersion: "1.0.0",
69+
expectedFullName: "some-template@1.0.0",
70+
},
71+
{
72+
name: "should return only the github url when no version specified",
73+
templateName:
74+
"https://github.com/NativeScript/template-drawer-navigation-ng#master",
75+
templateVersion: "",
76+
expectedFullName:
77+
"https://github.com/NativeScript/template-drawer-navigation-ng#master",
78+
},
79+
{
80+
name: "should return only the name when no version specified",
81+
templateName: "some-template",
82+
templateVersion: "",
83+
expectedFullName: "some-template",
84+
},
85+
].forEach((testCase) => {
86+
it(testCase.name, async () => {
87+
const testInjector = createTestInjector();
88+
const npm = testInjector.resolve<BunPackageManager>("bun");
89+
const templateFullName = await npm.getPackageFullName({
90+
name: testCase.templateName,
91+
version: testCase.templateVersion,
92+
});
93+
assert.strictEqual(templateFullName, testCase.expectedFullName);
94+
});
95+
});
96+
});
97+
});

0 commit comments

Comments
 (0)