Skip to content

Commit

Permalink
feat: add experimental bun package manager support (#5791)
Browse files Browse the repository at this point in the history

Co-authored-by: Igor Randjelovic <rigor789@gmail.com>
  • Loading branch information
jasongitmail and rigor789 committed Apr 3, 2024
1 parent 7c87b49 commit f758f6c
Show file tree
Hide file tree
Showing 11 changed files with 533 additions and 270 deletions.
1 change: 1 addition & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,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
1 change: 1 addition & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,4 +492,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: "some-template@1.0.0",
expectedVersion: "1.0.0",
expectedName: "some-template",
},
{
name: "should return both name and version when valid fullName with scope passed",
templateFullName: "@nativescript/some-template@1.0.0",
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: "some-template@1.0.0",
},
{
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);
});
});
});
});

0 comments on commit f758f6c

Please sign in to comment.