Skip to content

Commit 035436d

Browse files
committed
feat: add getPackageApi
1 parent 7fb7b1c commit 035436d

File tree

2 files changed

+248
-0
lines changed

2 files changed

+248
-0
lines changed

src/get-package-api.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { goTry } from "go-go-try";
2+
import { join } from "pathe";
3+
import { expect, test } from "vitest";
4+
import { getPackageApi } from "./get-package-api.ts";
5+
6+
test("invalid package name", async () => {
7+
const [err, _] = await goTry(getPackageApi({ pkg: "" }));
8+
expect(err).toBeDefined();
9+
});
10+
11+
test("invalid package name and empty subpath", async () => {
12+
const [err, _] = await goTry(getPackageApi({ pkg: "", subpath: "" }));
13+
expect(err).toBeDefined();
14+
});
15+
16+
test("npm package not found", async () => {
17+
const [err, _] = await goTry(getPackageApi({ pkg: "@jsdocs-io/not-found" }));
18+
expect(err).toBeDefined();
19+
});
20+
21+
test("npm package types not found", async () => {
22+
const [err, api] = await goTry(getPackageApi({ pkg: "unlicensed@0.4.0" }));
23+
expect(err).toBeUndefined();
24+
expect(api?.types).toBeUndefined();
25+
});
26+
27+
test("npm package successfully analyzed", async () => {
28+
const [err, api] = await goTry(getPackageApi({ pkg: "short-time-ago@2.0.0" }));
29+
expect(err).toBeUndefined();
30+
expect(api).toMatchObject({ name: "short-time-ago", version: "2.0.0" });
31+
});
32+
33+
test("local tarball package successfully analyzed", async () => {
34+
const [err, api] = await goTry(
35+
getPackageApi({ pkg: join(process.cwd(), "tarballs/short-time-ago-3.0.0.tgz") }),
36+
);
37+
expect(err).toBeUndefined();
38+
expect(api).toMatchObject({ name: "short-time-ago", version: "3.0.0" });
39+
});

src/get-package-api.ts

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { performance } from "node:perf_hooks";
2+
import { join } from "pathe";
3+
import { Bun } from "./bun.ts";
4+
import { getPackageDeclarations } from "./get-package-declarations.ts";
5+
import { getPackageJson } from "./get-package-json.ts";
6+
import { getPackageOverview } from "./get-package-overview.ts";
7+
import { getPackageTypes } from "./get-package-types.ts";
8+
import { getProject } from "./get-project.ts";
9+
import { tempDir } from "./temp-dir.ts";
10+
import type { ExtractedDeclaration } from "./types.ts";
11+
12+
/** `GetPackageApiOptions` contains the options for calling {@link getPackageApi}. */
13+
export interface GetPackageApiOptions {
14+
/**
15+
Package to extract the API from.
16+
17+
This can be either a package name (e.g., `foo`, `@foo/bar`) or
18+
any other query that can be passed to `bun add` (e.g., `foo@1.0.0`).
19+
20+
@see {@link https://bun.sh/docs/cli/add | Bun docs}
21+
*/
22+
pkg: string;
23+
24+
/**
25+
Specific subpath to consider in a package.
26+
27+
If a package has multiple entrypoints listed in the `exports` map property
28+
of its `package.json`, use `subpath` to select a specific one by its name
29+
(e.g., `someFeature`).
30+
31+
@defaultValue `.` (package root)
32+
33+
@see {@link https://nodejs.org/api/packages.html#subpath-exports | Node.js docs}
34+
@see {@link https://github.com/lukeed/resolve.exports | resolve.exports docs}
35+
*/
36+
subpath?: string;
37+
38+
/**
39+
Packages can have deeply nested modules and namespaces.
40+
41+
Use `maxDepth` to limit the depth of the extraction.
42+
Declarations nested at levels deeper than this value will be ignored.
43+
44+
@defaultValue 5
45+
*/
46+
maxDepth?: number;
47+
48+
/**
49+
Bun instance used to install the package.
50+
51+
@defaultValue a new `Bun` instance
52+
*/
53+
bun?: Bun;
54+
}
55+
56+
/** `PackageApi` contains the data extracted from a package by calling {@link getPackageApi}. */
57+
export interface PackageApi {
58+
/** Package name (e.g., `foo`, `@foo/bar`). */
59+
name: string;
60+
61+
/** Package version number (e.g., `1.0.0`). */
62+
version: string;
63+
64+
/**
65+
Package subpath selected when extracting the API (e.g., `.`, `someFeature`).
66+
67+
@see {@link ExtractPackageApiOptions.subpath}
68+
@see {@link https://nodejs.org/api/packages.html#subpath-exports | Node.js docs}
69+
*/
70+
subpath: string;
71+
72+
/**
73+
Type declarations file, resolved from the selected `subpath`,
74+
that acts as the entrypoint for the package (e.g., `index.d.ts`).
75+
*/
76+
types?: string;
77+
78+
/**
79+
Package description extracted from the `types` file if a
80+
JSDoc comment with the `@packageDocumentation` tag is found.
81+
*/
82+
overview?: string;
83+
84+
/** Declarations exported (or re-exported) by the package. */
85+
declarations: ExtractedDeclaration[];
86+
87+
/**
88+
All packages resolved and installed when installing the package (included).
89+
90+
@example
91+
```ts
92+
// Installing `foo` brings in also `bar` and `baz` as dependencies.
93+
["foo@1.0.0", "bar@2.0.0", "baz@3.0.0"]
94+
```
95+
*/
96+
dependencies: string[];
97+
98+
/** Timestamp of when the package was analyzed. */
99+
analyzedAt: string;
100+
101+
/** Package analysis duration in milliseconds. */
102+
analyzedIn: number;
103+
}
104+
105+
/**
106+
`getPackageApi` extracts the API from a package.
107+
108+
If the extraction succeeds, `getPackageApi` returns a {@link PackageApi} object.
109+
If the extraction fails, `getPackageApi` throws an error.
110+
111+
Warning: The extraction process is slow and blocks the main thread, using workers is recommended.
112+
113+
@example
114+
```ts
115+
const packageApi = await getPackageApi({
116+
pkg: "foo", // Extract API from npm package `foo` [Required]
117+
subpath: ".", // Select subpath `.` (root subpath) [Optional]
118+
maxDepth: 5, // Maximum depth for analyzing nested namespaces [Optional]
119+
bun: new Bun() // Bun package manager instance [Optional]
120+
});
121+
console.log(JSON.stringify(packageApi, null, 2));
122+
```
123+
124+
@param options - {@link GetPackageApiOptions}
125+
126+
@returns A {@link PackageApi} object
127+
*/
128+
export async function getPackageApi({
129+
pkg,
130+
subpath = ".",
131+
maxDepth = 5,
132+
bun = new Bun(),
133+
}: GetPackageApiOptions): Promise<PackageApi> {
134+
// Normalize options.
135+
pkg = pkg.trim();
136+
subpath = subpath.trim() || ".";
137+
maxDepth = Math.max(1, Math.round(maxDepth));
138+
139+
// Start performance timer.
140+
const start = performance.now();
141+
142+
// Create a temporary directory where to install the package.
143+
await using dir = await tempDir();
144+
const cwd = dir.path;
145+
146+
// Install the package and its direct and third-party dependencies.
147+
const dependencies = await bun.add(pkg, cwd);
148+
149+
// Read the package's `package.json`.
150+
const pkgName = await getInstalledPackageName(cwd);
151+
const pkgDir = join(cwd, "node_modules", pkgName);
152+
const pkgJson = await getPackageJson(pkgDir);
153+
const { name, version } = pkgJson;
154+
155+
// Find the package's types entry point file, if any.
156+
const types = getPackageTypes({ pkgJson, subpath });
157+
if (!types) {
158+
return {
159+
name,
160+
version,
161+
subpath,
162+
types: undefined,
163+
overview: undefined,
164+
declarations: [],
165+
dependencies,
166+
analyzedAt: analyzedAt(),
167+
analyzedIn: analyzedIn(start),
168+
};
169+
}
170+
171+
// Create TypeScript project.
172+
const pkgTypes = join(pkgDir, types);
173+
const { project, indexFile } = getProject({ indexFilePath: pkgTypes, typeRoots: cwd });
174+
175+
// Get overview.
176+
const overview = getPackageOverview(indexFile);
177+
178+
// Extract the declarations exported by the package.
179+
const declarations = await getPackageDeclarations({ pkgName, project, indexFile, maxDepth });
180+
181+
// Return the data extracted from the package.
182+
return {
183+
name,
184+
version,
185+
subpath,
186+
types,
187+
overview,
188+
declarations,
189+
dependencies,
190+
analyzedAt: analyzedAt(),
191+
analyzedIn: analyzedIn(start),
192+
};
193+
}
194+
195+
async function getInstalledPackageName(cwd: string): Promise<string> {
196+
// Since `pkg` can contain any argument accepted by Bun's `add` command
197+
// (e.g., URLs), get the package name from the only dependency listed in
198+
// the `package.json` file created by Bun in the `cwd` on install.
199+
const pkgJson = await getPackageJson(cwd);
200+
return Object.keys(pkgJson.dependencies!).at(0)!;
201+
}
202+
203+
function analyzedAt(): string {
204+
return new Date().toISOString();
205+
}
206+
207+
function analyzedIn(start: number): number {
208+
return Math.round(performance.now() - start);
209+
}

0 commit comments

Comments
 (0)