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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy