From ba52e1ee3f0317b81ed2f8708609fd36259e70dd Mon Sep 17 00:00:00 2001 From: Edoardo Scibona <12040076+velut@users.noreply.github.com> Date: Thu, 14 Mar 2024 22:19:05 +0100 Subject: [PATCH] feat: add PackageManager and update extractPackageApiEffect --- src/bun-package-manager.test.ts | 32 +++++++++++++++++++++++++ src/bun-package-manager.ts | 29 +++++++++++++++++++++++ src/extract-package-api-effect.ts | 11 +++++---- src/extract-package-api.ts | 8 ++++++- src/index.ts | 3 ++- src/install-package.test.ts | 29 ----------------------- src/install-package.ts | 39 ------------------------------- src/package-manager.ts | 23 ++++++++++++++++++ 8 files changed, 99 insertions(+), 75 deletions(-) create mode 100644 src/bun-package-manager.test.ts create mode 100644 src/bun-package-manager.ts delete mode 100644 src/install-package.test.ts delete mode 100644 src/install-package.ts create mode 100644 src/package-manager.ts diff --git a/src/bun-package-manager.test.ts b/src/bun-package-manager.test.ts new file mode 100644 index 0000000..91f9d17 --- /dev/null +++ b/src/bun-package-manager.test.ts @@ -0,0 +1,32 @@ +import { Effect } from "effect"; +import { temporaryDirectoryTask } from "tempy"; +import { expect, test } from "vitest"; +import { bunPackageManager } from "./bun-package-manager"; +import type { InstallPackageOptions } from "./package-manager"; + +const bun = bunPackageManager(); + +const _installPackage = (options: InstallPackageOptions) => + Effect.runPromise(bun.installPackage(options)); + +test("invalid package", async () => { + await temporaryDirectoryTask(async (cwd) => { + await expect(_installPackage({ pkg: "", cwd })).rejects.toThrow(); + }); +}); + +test("package with no production dependencies", async () => { + await temporaryDirectoryTask(async (cwd) => { + await expect(_installPackage({ pkg: "verify-hcaptcha@1.0.0", cwd })).resolves.toStrictEqual([ + "verify-hcaptcha@1.0.0", + ]); + }); +}); + +test("package with some production dependencies", async () => { + await temporaryDirectoryTask(async (cwd) => { + await expect(_installPackage({ pkg: "query-registry@2.6.0", cwd })).resolves.toContain( + "query-registry@2.6.0", + ); + }); +}); diff --git a/src/bun-package-manager.ts b/src/bun-package-manager.ts new file mode 100644 index 0000000..5d986d8 --- /dev/null +++ b/src/bun-package-manager.ts @@ -0,0 +1,29 @@ +import { Effect } from "effect"; +import { execa } from "execa"; +import { InstallPackageError, PackageManager } from "./package-manager"; + +/** @internal */ +export const bunPackageManager = (bunPath = "bun") => + PackageManager.of({ + installPackage: ({ pkg, cwd }) => + Effect.gen(function* (_) { + // Run `bun add --verbose`. + // See https://bun.sh/docs/cli/add. + const { stdout } = yield* _( + Effect.tryPromise({ + try: () => execa(bunPath, ["add", pkg, "--verbose"], { cwd }), + catch: (e) => new InstallPackageError({ cause: e }), + }), + ); + + // With verbose output on, bun prints one line per installed package + // (e.g., "foo@1.0.0"), including all installed dependencies. + // These lines are between the two delimiting lines found here: + // https://github.com/oven-sh/bun/blob/972a7b7080bd3066b54dcb43e9c91c5dfa26a69c/src/install/lockfile.zig#L5369-L5370. + const lines = stdout.split("\n"); + const beginHash = lines.findIndex((line) => line.startsWith("-- BEGIN SHA512/256")); + const endHash = lines.findIndex((line) => line.startsWith("-- END HASH")); + const installedPackages = lines.slice(beginHash + 1, endHash); + return installedPackages; + }), + }); diff --git a/src/extract-package-api-effect.ts b/src/extract-package-api-effect.ts index b7c9226..7a4b2aa 100644 --- a/src/extract-package-api-effect.ts +++ b/src/extract-package-api-effect.ts @@ -3,9 +3,9 @@ import { performance } from "node:perf_hooks"; import { join } from "pathe"; import { createProject } from "./create-project"; import type { ExtractPackageApiOptions, PackageApi } from "./extract-package-api"; -import { installPackage } from "./install-package"; import { packageDeclarations } from "./package-declarations"; import { packageJson } from "./package-json"; +import { PackageManager } from "./package-manager"; import { packageName } from "./package-name"; import { packageOverview } from "./package-overview"; import { packageTypes } from "./package-types"; @@ -16,13 +16,13 @@ export const extractPackageApiEffect = ({ pkg, subpath = ".", maxDepth = 5, - bunPath = "bun", }: ExtractPackageApiOptions) => Effect.gen(function* (_) { const startTime = performance.now(); const pkgName = yield* _(packageName(pkg)); const { path: cwd } = yield* _(workDir); - const packages = yield* _(installPackage({ pkg, cwd, bunPath })); + const pm = yield* _(PackageManager); + const packages = yield* _(pm.installPackage({ pkg, cwd })); const pkgDir = join(cwd, "node_modules", pkgName); const pkgJson = yield* _(packageJson(pkgDir)); const types = yield* _(packageTypes(pkgJson, subpath)); @@ -30,7 +30,7 @@ export const extractPackageApiEffect = ({ const { project, indexFile } = yield* _(createProject({ indexFilePath, cwd })); const overview = packageOverview(indexFile); const declarations = yield* _(packageDeclarations({ pkgName, project, indexFile, maxDepth })); - return { + const pkgApi: PackageApi = { name: pkgJson.name, version: pkgJson.version, subpath, @@ -40,5 +40,6 @@ export const extractPackageApiEffect = ({ packages, analyzedAt: new Date().toISOString(), analyzedIn: Math.round(performance.now() - startTime), - } satisfies PackageApi; + }; + return pkgApi; }); diff --git a/src/extract-package-api.ts b/src/extract-package-api.ts index 646f918..dc00f71 100644 --- a/src/extract-package-api.ts +++ b/src/extract-package-api.ts @@ -1,6 +1,8 @@ import { Effect } from "effect"; +import { bunPackageManager } from "./bun-package-manager"; import type { ExtractedDeclaration } from "./extract-declarations"; import { extractPackageApiEffect } from "./extract-package-api-effect"; +import { PackageManager } from "./package-manager"; /** `ExtractPackageApiOptions` contains all the options @@ -129,4 +131,8 @@ export const extractPackageApi = ({ maxDepth = 5, bunPath = "bun", }: ExtractPackageApiOptions): Promise => - Effect.runPromise(Effect.scoped(extractPackageApiEffect({ pkg, subpath, maxDepth, bunPath }))); + extractPackageApiEffect({ pkg, subpath, maxDepth }).pipe( + Effect.scoped, + Effect.provideService(PackageManager, bunPackageManager(bunPath)), + Effect.runPromise, + ); diff --git a/src/index.ts b/src/index.ts index d4a13a4..4269c9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export type { AllExtractedDeclaration, AllExtractedDeclarationKind, } from "./all-extracted-declaration"; +export { bunPackageManager } from "./bun-package-manager"; export { ProjectError } from "./create-project"; export type { ExtractedClass, @@ -36,9 +37,9 @@ export { export { extractPackageApiEffect } from "./extract-package-api-effect"; export type { ExtractedTypeAlias } from "./extract-type-alias"; export type { ExtractedVariable } from "./extract-variable"; -export { InstallPackageError, installPackage, type InstallPackageOptions } from "./install-package"; export { PackageDeclarationsError } from "./package-declarations"; export { PackageJsonError, packageJson } from "./package-json"; +export { InstallPackageError, PackageManager, type InstallPackageOptions } from "./package-manager"; export { PackageNameError, packageName } from "./package-name"; export { PackageTypesError, packageTypes } from "./package-types"; export { parseDocComment } from "./parse-doc-comment"; diff --git a/src/install-package.test.ts b/src/install-package.test.ts deleted file mode 100644 index da0d898..0000000 --- a/src/install-package.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Effect } from "effect"; -import { temporaryDirectoryTask } from "tempy"; -import { expect, test } from "vitest"; -import { installPackage, type InstallPackageOptions } from "./install-package"; - -const _installPackage = (options: InstallPackageOptions) => - Effect.runPromise(installPackage(options)); - -test("invalid package", async () => { - await temporaryDirectoryTask(async (dir) => { - await expect(_installPackage({ pkg: "", cwd: dir })).rejects.toThrow(); - }); -}); - -test("package with no production dependencies", async () => { - await temporaryDirectoryTask(async (dir) => { - await expect( - _installPackage({ pkg: "verify-hcaptcha@1.0.0", cwd: dir }), - ).resolves.toStrictEqual(["verify-hcaptcha@1.0.0"]); - }); -}); - -test("package with some production dependencies", async () => { - await temporaryDirectoryTask(async (dir) => { - await expect(_installPackage({ pkg: "query-registry@2.6.0", cwd: dir })).resolves.toContain( - "query-registry@2.6.0", - ); - }); -}); diff --git a/src/install-package.ts b/src/install-package.ts deleted file mode 100644 index b34d268..0000000 --- a/src/install-package.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Data, Effect } from "effect"; -import { execa } from "execa"; - -/** @internal */ -export type InstallPackageOptions = { - pkg: string; - cwd: string; - bunPath?: string; -}; - -/** @internal */ -export class InstallPackageError extends Data.TaggedError("InstallPackageError")<{ - cause?: unknown; -}> {} - -/** @internal */ -export const installPackage = ({ pkg, cwd, bunPath = "bun" }: InstallPackageOptions) => - Effect.gen(function* (_) { - const { stdout } = yield* _(bunAdd({ pkg, cwd, bunPath })); - return installedPackages(stdout); - }); - -const bunAdd = ({ pkg, cwd, bunPath }: Required) => - Effect.tryPromise({ - try: () => execa(bunPath, ["add", pkg, "--verbose"], { cwd }), - catch: (e) => new InstallPackageError({ cause: e }), - }); - -const installedPackages = (stdout: string) => { - // With verbose output on, bun prints one line per installed package - // (for example, "foo@1.0.0"), including all installed dependencies. - // These lines are between the two delimiting lines found here: - // https://github.com/oven-sh/bun/blob/972a7b7080bd3066b54dcb43e9c91c5dfa26a69c/src/install/lockfile.zig#L5369-L5370. - const lines = stdout.split("\n"); - const beginHash = lines.findIndex((line) => line.startsWith("-- BEGIN SHA512/256")); - const endHash = lines.findIndex((line) => line.startsWith("-- END HASH")); - const installedPackages = lines.slice(beginHash + 1, endHash); - return installedPackages; -}; diff --git a/src/package-manager.ts b/src/package-manager.ts new file mode 100644 index 0000000..c1af516 --- /dev/null +++ b/src/package-manager.ts @@ -0,0 +1,23 @@ +import { Context, Data, Effect } from "effect"; + +/** @internal */ +export type InstallPackageOptions = { + pkg: string; + cwd: string; +}; + +/** @internal */ +export class InstallPackageError extends Data.TaggedError("InstallPackageError")<{ + cause?: unknown; +}> {} + +/** @internal */ +export class PackageManager extends Context.Tag("PackageManager")< + PackageManager, + { + readonly installPackage: ({ + pkg, + cwd, + }: InstallPackageOptions) => Effect.Effect; + } +>() {}