Skip to content

Commit 97ad911

Browse files
feat: retrieve owner email from npm or git config (#520)
<!-- 👋 Hi, thanks for sending a PR to template-typescript-node-package! 💖. Please fill out all fields below and make sure each item is true and [x] checked. Otherwise we may not be able to review your PR. --> ## PR Checklist - [x] Addresses an existing open issue: fixes #499 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/template-typescript-node-package/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/template-typescript-node-package/blob/main/.github/CONTRIBUTING.md) were taken ## Overview <!-- Description of what is changed and how the code change does that. --> - Extracted the logic of retrieving the npm user info to a new function and modified the tests accordingly - `getHydrationValues` will try to retrieve the email from the npm user info, and in case of failure, will reach out to git config. * Note: I was not sure how to properly test the new functionality rather than modifying the tests, please advice, or check the new functionality to provide me with feedback. --------- Co-authored-by: Josh Goldberg ✨ <[email protected]>
1 parent 4ee744b commit 97ad911

File tree

8 files changed

+245
-82
lines changed

8 files changed

+245
-82
lines changed

src/hydrate/values/getHydrationDefaults.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
import { $ } from "execa";
22

3+
import type { PartialPackageData } from "./types.js";
4+
35
import { getNpmAuthor } from "../../shared/getNpmAuthor.js";
46
import { readFileSafe } from "../readFileSafe.js";
7+
import { readEmailIfExists } from "./readEmailIfExists.js";
58
import { readFundingIfExists } from "./readFundingIfExists.js";
69

7-
interface PartialPackageData {
8-
author?: { email: string; name: string } | string;
9-
description?: string;
10-
email?: string;
11-
name?: string;
12-
repository?: string;
13-
}
14-
1510
export async function getHydrationDefaults() {
1611
const existingReadme = await readFileSafe("./README.md", "");
1712
const existingPackage = JSON.parse(
@@ -27,10 +22,7 @@ export async function getHydrationDefaults() {
2722

2823
return fromPackage ?? (await getNpmAuthor());
2924
},
30-
email:
31-
typeof existingPackage.author === "string"
32-
? existingPackage.author.split(/<|>/)[1]
33-
: existingPackage.author?.email,
25+
email: () => readEmailIfExists(existingPackage),
3426
funding: readFundingIfExists,
3527
owner: async () =>
3628
(await $`git remote -v`).stdout.match(
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { readEmailIfExists } from "./readEmailIfExists.js";
4+
5+
const validAuthorPackage = {
6+
author: "Author<[email protected]>",
7+
8+
};
9+
10+
const validEmailPackage = {
11+
author: { email: "[email protected]", name: "Author" },
12+
};
13+
14+
const mock$ = vi.fn();
15+
16+
vi.mock("execa", () => ({
17+
get $() {
18+
return mock$;
19+
},
20+
}));
21+
22+
const mockGetNpmUserInfo = vi.fn();
23+
24+
vi.mock("../../shared/getNpmUserInfo.js", () => ({
25+
get getNpmUserInfo() {
26+
return mockGetNpmUserInfo;
27+
},
28+
}));
29+
30+
describe("readEmailIfExists", () => {
31+
it('reads email from the package "author" field when it exists', async () => {
32+
const email = await readEmailIfExists(validAuthorPackage);
33+
34+
expect(email).toBe("[email protected]");
35+
});
36+
37+
it("reads email from the email field in an author object when it exists", async () => {
38+
const email = await readEmailIfExists(validEmailPackage);
39+
40+
expect(email).toBe("[email protected]");
41+
});
42+
43+
it("reads email from getNpmUserInfo when available and no author information exists", async () => {
44+
mockGetNpmUserInfo.mockResolvedValue({
45+
succeeded: true,
46+
value: {
47+
avatar: "",
48+
49+
github: "",
50+
name: "someone",
51+
twitter: "",
52+
},
53+
});
54+
55+
const email = await readEmailIfExists({});
56+
expect(email).toBe("[email protected]");
57+
});
58+
59+
it("reads email from git config when it exists and nothing else worked", async () => {
60+
mockGetNpmUserInfo.mockResolvedValue({
61+
reason: "it doesn't matter",
62+
succeeded: false,
63+
});
64+
mock$.mockResolvedValue({ stdout: "[email protected]" });
65+
const email = await readEmailIfExists({});
66+
67+
expect(email).toBe("[email protected]");
68+
});
69+
70+
it("return undefined when nothing worked", async () => {
71+
mockGetNpmUserInfo.mockResolvedValue({
72+
reason: "it doesn't matter",
73+
succeeded: false,
74+
});
75+
mock$.mockRejectedValue("");
76+
const email = await readEmailIfExists({});
77+
expect(email).toBe(undefined);
78+
});
79+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { $ } from "execa";
2+
3+
import type { PartialPackageData } from "./types.js";
4+
5+
import { getNpmUserInfo } from "../../shared/getNpmUserInfo.js";
6+
7+
export async function readEmailIfExists(
8+
existingPackage: PartialPackageData
9+
): Promise<string | undefined> {
10+
const fromPackage =
11+
typeof existingPackage.author === "string"
12+
? existingPackage.author.split(/<|>/)[1]
13+
: existingPackage.author?.email;
14+
if (fromPackage) {
15+
return fromPackage;
16+
}
17+
18+
const result = await getNpmUserInfo();
19+
if (result.succeeded) {
20+
return result.value.email;
21+
}
22+
23+
try {
24+
const { stdout } = await $`git config --get user.email`;
25+
return stdout;
26+
} catch {
27+
return undefined;
28+
}
29+
}

src/hydrate/values/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ export interface HydrationInputValues extends InputValues {
44
author: string;
55
email: string;
66
}
7+
8+
export interface PartialPackageData {
9+
author?: { email: string; name: string } | string;
10+
description?: string;
11+
email?: string;
12+
name?: string;
13+
repository?: string;
14+
}

src/shared/getNpmAuthor.test.ts

Lines changed: 26 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,16 @@ import { SpyInstance, beforeEach, describe, expect, it, vi } from "vitest";
33

44
import { getNpmAuthor } from "./getNpmAuthor.js";
55

6-
const mock$ = vi.fn();
6+
const mockNpmUserInfo = vi.fn();
77

8-
vi.mock("execa", () => ({
9-
get $() {
10-
return mock$;
11-
},
12-
}));
13-
14-
const mockNpmUser = vi.fn();
15-
16-
vi.mock("npm-user", () => ({
17-
get default() {
18-
return mockNpmUser;
8+
vi.mock("./getNpmUserInfo", () => ({
9+
get getNpmUserInfo() {
10+
return mockNpmUserInfo;
1911
},
2012
}));
2113

2214
let mockConsoleLog: SpyInstance;
2315

24-
const npmUsername = "test-npm-username";
2516
const owner = "test-owner";
2617

2718
describe("getNpmAuthor", () => {
@@ -31,48 +22,42 @@ describe("getNpmAuthor", () => {
3122
.mockImplementation(() => undefined);
3223
});
3324

34-
it("logs and defaults to owner when npm whoami fails", async () => {
35-
mock$.mockRejectedValue({ stderr: "Oh no!" });
25+
it("logs and defaults to owner when getNpmUserInfo fails", async () => {
26+
mockNpmUserInfo.mockResolvedValue({
27+
reason: "Some reason",
28+
succeeded: false,
29+
});
3630

3731
const author = await getNpmAuthor(owner);
3832

3933
expect(author).toBe(owner);
40-
expect(mockConsoleLog).toHaveBeenCalledWith(chalk.gray("│"));
4134
expect(mockConsoleLog).toHaveBeenCalledWith(
42-
[
43-
chalk.gray("│"),
44-
chalk.gray("Could not populate npm user. Failed to run npm whoami."),
45-
].join(" ")
35+
[chalk.gray("│"), chalk.gray("Some reason")].join(" ")
4636
);
4737
});
4838

49-
it("logs and defaults to owner when retrieving the npm whoami user fails", async () => {
50-
mock$.mockResolvedValue({ stdout: npmUsername });
51-
mockNpmUser.mockRejectedValue("Oh no!");
39+
it("returns npm user info with only name when no email available for npm user", async () => {
40+
const name = "Test Author";
41+
mockNpmUserInfo.mockResolvedValue({
42+
succeeded: true,
43+
value: { name },
44+
});
5245

5346
const author = await getNpmAuthor(owner);
54-
55-
expect(author).toBe(owner);
56-
expect(mockConsoleLog).toHaveBeenCalledWith(chalk.gray("│"));
57-
expect(mockConsoleLog).toHaveBeenCalledWith(
58-
[
59-
chalk.gray("│"),
60-
chalk.gray(
61-
"Could not populate npm user. Failed to retrieve user info from npm."
62-
),
63-
].join(" ")
64-
);
47+
expect(author).toBe(name);
48+
expect(mockConsoleLog).not.toHaveBeenCalled();
6549
});
6650

67-
it("returns npm user info when retrieving the npm whoami user succeeds", async () => {
68-
const name = "Test Author <[email protected]>";
69-
70-
mock$.mockResolvedValue({ stdout: npmUsername });
71-
mockNpmUser.mockResolvedValue({ name });
51+
it("returns npm user info when getNpmUserInfo succeeds and contains all information", async () => {
52+
const name = "Test Author";
53+
const email = "<[email protected]>";
54+
mockNpmUserInfo.mockResolvedValue({
55+
succeeded: true,
56+
value: { email, name },
57+
});
7258

7359
const author = await getNpmAuthor(owner);
74-
75-
expect(author).toBe(name);
60+
expect(author).toBe(`${name} <${email}>`);
7661
expect(mockConsoleLog).not.toHaveBeenCalled();
7762
});
7863
});

src/shared/getNpmAuthor.ts

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,21 @@
11
import chalk from "chalk";
2-
import { $ } from "execa";
3-
import npmUser from "npm-user";
42

53
import { logLine } from "./cli/lines.js";
4+
import { getNpmUserInfo } from "./getNpmUserInfo.js";
65

76
export async function getNpmAuthor(): Promise<string | undefined>;
87
export async function getNpmAuthor(owner: string): Promise<string>;
98
export async function getNpmAuthor(
109
owner?: string | undefined
1110
): Promise<string | undefined> {
12-
let username;
13-
14-
try {
15-
const { stdout } = await $`npm whoami`;
16-
17-
username = stdout;
18-
} catch {
11+
const result = await getNpmUserInfo();
12+
if (!result.succeeded) {
1913
logLine();
20-
logLine(
21-
chalk.gray("Could not populate npm user. Failed to run npm whoami.")
22-
);
23-
24-
return owner;
25-
}
26-
27-
let npmUserInfo;
28-
29-
try {
30-
npmUserInfo = await npmUser(username);
31-
} catch {
32-
logLine();
33-
logLine(
34-
chalk.gray(
35-
"Could not populate npm user. Failed to retrieve user info from npm."
36-
)
37-
);
38-
14+
logLine(chalk.gray(result.reason));
3915
return owner;
4016
}
4117

42-
const { email, name = owner } = npmUserInfo;
18+
const { email, name = owner } = result.value;
4319
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4420
return email ? `${name!} <${email}>` : name;
4521
}

src/shared/getNpmUserInfo.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { getNpmUserInfo } from "./getNpmUserInfo.js";
4+
5+
const mock$ = vi.fn();
6+
7+
vi.mock("execa", () => ({
8+
get $() {
9+
return mock$;
10+
},
11+
}));
12+
13+
const mockNpmUser = vi.fn();
14+
15+
vi.mock("npm-user", () => ({
16+
get default() {
17+
return mockNpmUser;
18+
},
19+
}));
20+
21+
const npmUsername = "test-npm-username";
22+
23+
describe("getNpmUserInfo", () => {
24+
it("returns an error result when npm whoami fails", async () => {
25+
mock$.mockRejectedValue({ stderr: "Oh no!" });
26+
const result = await getNpmUserInfo();
27+
28+
expect(result).toEqual({
29+
reason: "Could not populate npm user. Failed to run npm whoami.",
30+
succeeded: false,
31+
});
32+
});
33+
34+
it("returns a success result when the npm whoami user succeeds", async () => {
35+
mock$.mockResolvedValue({ stdout: npmUsername });
36+
mockNpmUser.mockResolvedValue({ name: npmUsername });
37+
const result = await getNpmUserInfo();
38+
39+
expect(result).toEqual({
40+
succeeded: true,
41+
value: { name: npmUsername },
42+
});
43+
});
44+
45+
it("returns an error result the npm whoami user fails", async () => {
46+
mock$.mockResolvedValue({ stdout: npmUsername });
47+
mockNpmUser.mockRejectedValue("error");
48+
const result = await getNpmUserInfo();
49+
50+
expect(result).toEqual({
51+
reason:
52+
"Could not populate npm user. Failed to retrieve user info from npm.",
53+
succeeded: false,
54+
});
55+
});
56+
});

0 commit comments

Comments
 (0)