Skip to content

feat: add experimental bun package manager support #5791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ injector.requirePublic("npm", "./node-package-manager");
injector.requirePublic("yarn", "./yarn-package-manager");
injector.requirePublic("yarn2", "./yarn2-package-manager");
injector.requirePublic("pnpm", "./pnpm-package-manager");
injector.requirePublic("bun", "./bun-package-manager");
injector.requireCommand(
"package-manager|*get",
"./commands/package-manager-get"
Expand Down
158 changes: 158 additions & 0 deletions lib/bun-package-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import * as path from "path";
import { BasePackageManager } from "./base-package-manager";
import { exported, cache } from "./common/decorators";
import { CACACHE_DIRECTORY_NAME } from "./constants";
import * as _ from "lodash";
import {
INodePackageManagerInstallOptions,
INpmInstallResultInfo,
INpmsResult,
} from "./declarations";
import {
IChildProcess,
IErrors,
IFileSystem,
IHostInfo,
Server,
} from "./common/declarations";
import { injector } from "./common/yok";

export class BunPackageManager extends BasePackageManager {
constructor(
$childProcess: IChildProcess,
private $errors: IErrors,
$fs: IFileSystem,
$hostInfo: IHostInfo,
private $logger: ILogger,
private $httpClient: Server.IHttpClient,
$pacoteService: IPacoteService
) {
super($childProcess, $fs, $hostInfo, $pacoteService, "bun");
}

@exported("bun")
public async install(
packageName: string,
pathToSave: string,
config: INodePackageManagerInstallOptions
): Promise<INpmInstallResultInfo> {
if (config.disableNpmInstall) {
return;
}
if (config.ignoreScripts) {
config["ignore-scripts"] = true;
}

const packageJsonPath = path.join(pathToSave, "package.json");
const jsonContentBefore = this.$fs.readJson(packageJsonPath);

const flags = this.getFlagsString(config, true);
// TODO: Confirm desired behavior. The npm version uses --legacy-peer-deps
// by default, we could use `--no-peer` for Bun if similar is needed; the
// pnpm version uses `--shamefully-hoist`, but Bun has no similar flag.
let params = ["install", "--legacy-peer-deps"];
const isInstallingAllDependencies = packageName === pathToSave;
if (!isInstallingAllDependencies) {
params.push(packageName);
}

params = params.concat(flags);
const cwd = pathToSave;

try {
const result = await this.processPackageManagerInstall(
packageName,
params,
{ cwd, isInstallingAllDependencies }
);
return result;
} catch (err) {
// Revert package.json contents to preserve valid state
this.$fs.writeJson(packageJsonPath, jsonContentBefore);
throw err;
}
}

@exported("bun")
public async uninstall(
packageName: string,
config?: any,
cwd?: string
): Promise<string> {
const flags = this.getFlagsString(config, false);
return this.$childProcess.exec(`bun remove ${packageName} ${flags}`, {
cwd,
});
}

// Bun does not have a `view` command; use npm.
@exported("bun")
public async view(packageName: string, config: Object): Promise<any> {
const wrappedConfig = _.extend({}, config, { json: true }); // always require view response as JSON

const flags = this.getFlagsString(wrappedConfig, false);
let viewResult: any;
try {
viewResult = await this.$childProcess.exec(
`npm view ${packageName} ${flags}`
);
} catch (e) {
this.$errors.fail(e.message);
}

try {
return JSON.parse(viewResult);
} catch (err) {
return null;
}
}

// Bun does not have a `search` command; use npm.
@exported("bun")
public async search(filter: string[], config: any): Promise<string> {
const flags = this.getFlagsString(config, false);
return this.$childProcess.exec(`npm search ${filter.join(" ")} ${flags}`);
}

public async searchNpms(keyword: string): Promise<INpmsResult> {
// Bugs with npms.io:
// 1. API returns no results when a valid package name contains @ or /
// even if using encodeURIComponent().
// 2. npms.io's API no longer returns updated results; see
// https://github.com/npms-io/npms-api/issues/112. Better to switch to
// https://registry.npmjs.org/<query>
const httpRequestResult = await this.$httpClient.httpRequest(
`https://api.npms.io/v2/search?q=keywords:${keyword}`
);
const result: INpmsResult = JSON.parse(httpRequestResult.body);
return result;
}

// Bun does not have a command analogous to `npm config get registry`; Bun
// uses `bunfig.toml` to define custom registries.
// - TODO: read `bunfig.toml`, if it exists, and return the registry URL.
public async getRegistryPackageData(packageName: string): Promise<any> {
const registry = await this.$childProcess.exec(`npm config get registry`);
const url = registry.trim() + packageName;
this.$logger.trace(
`Trying to get data from npm registry for package ${packageName}, url is: ${url}`
);
const responseData = (await this.$httpClient.httpRequest(url)).body;
this.$logger.trace(
`Successfully received data from npm registry for package ${packageName}. Response data is: ${responseData}`
);
const jsonData = JSON.parse(responseData);
this.$logger.trace(
`Successfully parsed data from npm registry for package ${packageName}.`
);
return jsonData;
}

@cache()
public async getCachePath(): Promise<string> {
const cachePath = await this.$childProcess.exec(`bun pm cache`);
return path.join(cachePath.trim(), CACACHE_DIRECTORY_NAME);
}
}

injector.register("bun", BunPackageManager);
12 changes: 8 additions & 4 deletions lib/commands/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,24 @@ export class PreviewCommand implements ICommand {
const previewCLIPath = this.getPreviewCLIPath();

if (!previewCLIPath) {
const packageManagerName = await this.$packageManager.getPackageManagerName();
const packageManagerName =
await this.$packageManager.getPackageManagerName();
let installCommand = "";

switch (packageManagerName) {
case PackageManagers.npm:
installCommand = "npm install --save-dev @nativescript/preview-cli";
break;
case PackageManagers.yarn:
case PackageManagers.yarn2:
installCommand = "yarn add -D @nativescript/preview-cli";
break;
case PackageManagers.pnpm:
installCommand = "pnpm install --save-dev @nativescript/preview-cli";
break;
case PackageManagers.bun:
installCommand = "bun add --dev @nativescript/preview-cli";
case PackageManagers.npm:
default:
installCommand = "npm install --save-dev @nativescript/preview-cli";
break;
}
this.$logger.info(
[
Expand Down
10 changes: 7 additions & 3 deletions lib/common/dispatchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,20 @@ export class CommandDispatcher implements ICommandDispatcher {
let updateCommand = "";

switch (packageManagerName) {
case PackageManagers.npm:
updateCommand = "npm i -g nativescript";
break;
case PackageManagers.yarn:
case PackageManagers.yarn2:
updateCommand = "yarn global add nativescript";
break;
case PackageManagers.pnpm:
updateCommand = "pnpm i -g nativescript";
break;
case PackageManagers.bun:
updateCommand = "bun add --global nativescript";
break;
case PackageManagers.npm:
default:
updateCommand = "npm i -g nativescript";
break;
}

if (
Expand Down
7 changes: 5 additions & 2 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,17 @@ export class ITMSConstants {
}

class ItunesConnectApplicationTypesClass
implements IiTunesConnectApplicationType {
implements IiTunesConnectApplicationType
{
public iOS = "iOS App";
public Mac = "Mac OS X App";
}

export const iOSAppResourcesFolderName = "iOS";
export const androidAppResourcesFolderName = "Android";

export const ItunesConnectApplicationTypes = new ItunesConnectApplicationTypesClass();
export const ItunesConnectApplicationTypes =
new ItunesConnectApplicationTypesClass();
export const VUE_NAME = "vue";
export const ANGULAR_NAME = "angular";
export const JAVASCRIPT_NAME = "javascript";
Expand Down Expand Up @@ -478,4 +480,5 @@ export enum PackageManagers {
pnpm = "pnpm",
yarn = "yarn",
yarn2 = "yarn2",
bun = "bun",
}
9 changes: 6 additions & 3 deletions lib/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class PackageManager implements IPackageManager {
private $yarn: INodePackageManager,
private $yarn2: INodePackageManager,
private $pnpm: INodePackageManager,
private $bun: INodePackageManager,
private $logger: ILogger,
private $userSettingsService: IUserSettingsService,
private $projectConfigService: IProjectConfigService
Expand Down Expand Up @@ -144,9 +145,8 @@ export class PackageManager implements IPackageManager {
}

try {
const configPm = this.$projectConfigService.getValue(
"cli.packageManager"
);
const configPm =
this.$projectConfigService.getValue("cli.packageManager");

if (configPm) {
this.$logger.trace(
Expand All @@ -172,6 +172,9 @@ export class PackageManager implements IPackageManager {
} else if (pm === PackageManagers.pnpm || this.$options.pnpm) {
this._packageManagerName = PackageManagers.pnpm;
return this.$pnpm;
} else if (pm === PackageManagers.bun) {
this._packageManagerName = PackageManagers.bun;
return this.$bun;
} else {
this._packageManagerName = PackageManagers.npm;
return this.$npm;
Expand Down
97 changes: 97 additions & 0 deletions test/bun-package-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Yok } from "../lib/common/yok";
import * as stubs from "./stubs";
import { assert } from "chai";
import { BunPackageManager } from "../lib/bun-package-manager";
import { IInjector } from "../lib/common/definitions/yok";

function createTestInjector(configuration: {} = {}): IInjector {
const injector = new Yok();
injector.register("hostInfo", {});
injector.register("errors", stubs.ErrorsStub);
injector.register("logger", stubs.LoggerStub);
injector.register("childProcess", stubs.ChildProcessStub);
injector.register("httpClient", {});
injector.register("fs", stubs.FileSystemStub);
injector.register("bun", BunPackageManager);
injector.register("pacoteService", {
manifest: () => Promise.resolve(),
});

return injector;
}

describe("node-package-manager", () => {
describe("getPackageNameParts", () => {
[
{
name: "should return both name and version when valid fullName passed",
templateFullName: "[email protected]",
expectedVersion: "1.0.0",
expectedName: "some-template",
},
{
name: "should return both name and version when valid fullName with scope passed",
templateFullName: "@nativescript/[email protected]",
expectedVersion: "1.0.0",
expectedName: "@nativescript/some-template",
},
{
name: "should return only name when version is not specified and the template is scoped",
templateFullName: "@nativescript/some-template",
expectedVersion: "",
expectedName: "@nativescript/some-template",
},
{
name: "should return only name when version is not specified",
templateFullName: "some-template",
expectedVersion: "",
expectedName: "some-template",
},
].forEach((testCase) => {
it(testCase.name, async () => {
const testInjector = createTestInjector();
const npm = testInjector.resolve<BunPackageManager>("bun");
const templateNameParts = await npm.getPackageNameParts(
testCase.templateFullName
);
assert.strictEqual(templateNameParts.name, testCase.expectedName);
assert.strictEqual(templateNameParts.version, testCase.expectedVersion);
});
});
});

describe("getPackageFullName", () => {
[
{
name: "should return name and version when specified",
templateName: "some-template",
templateVersion: "1.0.0",
expectedFullName: "[email protected]",
},
{
name: "should return only the github url when no version specified",
templateName:
"https://github.com/NativeScript/template-drawer-navigation-ng#master",
templateVersion: "",
expectedFullName:
"https://github.com/NativeScript/template-drawer-navigation-ng#master",
},
{
name: "should return only the name when no version specified",
templateName: "some-template",
templateVersion: "",
expectedFullName: "some-template",
},
].forEach((testCase) => {
it(testCase.name, async () => {
const testInjector = createTestInjector();
const npm = testInjector.resolve<BunPackageManager>("bun");
const templateFullName = await npm.getPackageFullName({
name: testCase.templateName,
version: testCase.templateVersion,
});
assert.strictEqual(templateFullName, testCase.expectedFullName);
});
});
});
});
Loading