Skip to content

Commit 0bdd69f

Browse files
fix: respect existing package.json contents in hydration (#509)
## PR Checklist - [x] Addresses an existing open issue: fixes #503 - [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 Splits the creation of new `package.json` contents into a new `writePackageJson` file. It reads the existing `package.json` as JSON and uses all previously existing fields by default. Note that this PR also standardizes from `JSON` to `Json` in file names.
1 parent 6e6ac28 commit 0bdd69f

File tree

10 files changed

+204
-71
lines changed

10 files changed

+204
-71
lines changed

src/hydrate/steps/writing/creation/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { createDotHusky } from "./dotHusky.js";
55
import { createDotVSCode } from "./dotVSCode.js";
66
import { createRootFiles } from "./rootFiles.js";
77

8-
export function createStructure(values: HydrationInputValues): Structure {
8+
export async function createStructure(
9+
values: HydrationInputValues
10+
): Promise<Structure> {
911
return {
1012
".github": createDotGitHub(values),
1113
".husky": createDotHusky(),
1214
".vscode": createDotVSCode(),
13-
...createRootFiles(values),
15+
...(await createRootFiles(values)),
1416
};
1517
}

src/hydrate/steps/writing/creation/rootFiles.ts

Lines changed: 14 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
11
import { HydrationInputValues } from "../../../values/types.js";
22
import { formatIgnoreFile } from "./formatters/formatIgnoreFile.js";
33
import { formatJson } from "./formatters/formatJson.js";
4+
import { writePackageJson } from "./writePackageJson.js";
45

5-
export function createRootFiles({
6-
author,
7-
description,
8-
email,
9-
owner,
10-
releases,
11-
repository,
12-
unitTests,
13-
}: HydrationInputValues) {
6+
export async function createRootFiles(values: HydrationInputValues) {
147
return {
158
".all-contributorsrc": formatJson({
169
badgeTemplate:
@@ -22,14 +15,14 @@ export function createRootFiles({
2215
contributorsSortAlphabetically: true,
2316
files: ["README.md"],
2417
imageSize: 100,
25-
projectName: repository,
26-
projectOwner: owner,
18+
projectName: values.repository,
19+
projectOwner: values.owner,
2720
repoHost: "https://github.com",
2821
repoType: "github",
2922
}),
3023
".eslintignore": formatIgnoreFile([
3124
"!.*",
32-
...(unitTests ? ["coverage"] : []),
25+
...(values.unitTests ? ["coverage"] : []),
3326
"lib",
3427
"node_modules",
3528
"pnpm-lock.yaml",
@@ -106,7 +99,7 @@ module.exports = {
10699
},
107100
extends: ["plugin:jsonc/recommended-with-json"],
108101
},${
109-
unitTests
102+
values.unitTests
110103
? `\n{
111104
files: "**/*.test.ts",
112105
rules: {
@@ -145,17 +138,17 @@ module.exports = {
145138
"@typescript-eslint",
146139
"deprecation",
147140
"import",
148-
"jsdoc",${unitTests ? `\n"no-only-tests",` : ""}
141+
"jsdoc",${values.unitTests ? `\n"no-only-tests",` : ""}
149142
"regexp",
150143
"simple-import-sort",
151-
"typescript-sort-keys",${unitTests ? `\n"vitest",` : ""}
144+
"typescript-sort-keys",${values.unitTests ? `\n"vitest",` : ""}
152145
],
153146
root: true,
154147
rules: {
155148
// These off/less-strict-by-default rules work well for this repo and we like them on.
156149
"@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "all" }],
157150
"import/extensions": ["error", "ignorePackages"],${
158-
unitTests ? `\n"no-only-tests/no-only-tests": "error",` : ""
151+
values.unitTests ? `\n"no-only-tests/no-only-tests": "error",` : ""
159152
}
160153
"simple-import-sort/exports": "error",
161154
"simple-import-sort/imports": "error",
@@ -175,7 +168,7 @@ module.exports = {
175168
};
176169
`,
177170
".gitignore": formatIgnoreFile([
178-
...(unitTests ? ["coverage/"] : []),
171+
...(values.unitTests ? ["coverage/"] : []),
179172
"lib/",
180173
"node_modules/",
181174
]),
@@ -199,7 +192,7 @@ module.exports = {
199192
}),
200193
".nvmrc": `18.16.0\n`,
201194
".prettierignore": formatIgnoreFile([
202-
...(unitTests ? ["coverage/"] : []),
195+
...(values.unitTests ? ["coverage/"] : []),
203196
"lib/",
204197
"pnpm-lock.yaml",
205198
"",
@@ -258,7 +251,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
258251
ignorePaths: [
259252
".github",
260253
"CHANGELOG.md",
261-
...(unitTests ? ["coverage"] : []),
254+
...(values.unitTests ? ["coverage"] : []),
262255
"lib",
263256
"node_modules",
264257
"pnpm-lock.yaml",
@@ -287,46 +280,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
287280
ignoreBinaries: ["dedupe", "gh"],
288281
project: ["src/**/*.ts!", "script/**/*.js"],
289282
}),
290-
"package.json": formatJson({
291-
name: repository,
292-
description,
293-
repository: {
294-
type: "git",
295-
url: `https://github.com/${owner}/${repository}`,
296-
},
297-
license: "MIT",
298-
author: { email, name: author },
299-
type: "module",
300-
main: "./lib/index.js",
301-
files: ["lib/", "package.json", "LICENSE.md", "README.md"],
302-
scripts: {
303-
build: "tsc",
304-
format: 'prettier "**/*" --ignore-unknown',
305-
"format:write": "pnpm format --write",
306-
lint: "eslint . --max-warnings 0 --report-unused-disable-directives",
307-
"lint:knip": "knip",
308-
"lint:md":
309-
'markdownlint "**/*.md" ".github/**/*.md" --rules sentences-per-line',
310-
"lint:package": "npmPkgJsonLint .",
311-
"lint:packages": "pnpm dedupe --check",
312-
"lint:spelling": 'cspell "**" ".github/**/*"',
313-
prepare: "husky install",
314-
...(releases && {
315-
"should-semantic-release": "should-semantic-release --verbose",
316-
}),
317-
...(unitTests && { test: "vitest" }),
318-
},
319-
"lint-staged": {
320-
"*": "prettier --ignore-unknown --write",
321-
},
322-
packageManager: "[email protected]",
323-
engines: {
324-
node: ">=18",
325-
},
326-
publishConfig: {
327-
provenance: true,
328-
},
329-
}),
283+
"package.json": await writePackageJson(values),
330284
"tsconfig.eslint.json": formatJson({
331285
extends: "./tsconfig.json",
332286
include: ["."],
@@ -346,7 +300,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
346300
},
347301
include: ["src"],
348302
}),
349-
...(unitTests && {
303+
...(values.unitTests && {
350304
"vitest.config.ts": `import { defineConfig } from "vitest/config";
351305
352306
export default defineConfig({
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { writePackageJson } from "./writePackageJson.js";
4+
5+
const mockReadFileAsJson = vi.fn();
6+
7+
vi.mock("../../../../shared/readFileAsJson.js", () => ({
8+
get readFileAsJson() {
9+
return mockReadFileAsJson;
10+
},
11+
}));
12+
13+
const values = {
14+
author: "test-author",
15+
description: "test-description",
16+
email: "test-email",
17+
owner: "test-owner",
18+
releases: false,
19+
repository: "test-repository",
20+
unitTests: false,
21+
};
22+
23+
describe("writePackageJson", () => {
24+
it("preserves existing dependencies when they exist", async () => {
25+
const dependencies = { abc: "1.2.3" };
26+
mockReadFileAsJson.mockResolvedValue({ dependencies });
27+
28+
const packageJson = await writePackageJson(values);
29+
30+
expect(JSON.parse(packageJson)).toEqual(
31+
expect.objectContaining({ dependencies })
32+
);
33+
});
34+
35+
it("includes a release script when releases is true", async () => {
36+
mockReadFileAsJson.mockResolvedValue({});
37+
38+
const packageJson = await writePackageJson({
39+
...values,
40+
releases: true,
41+
});
42+
43+
expect(JSON.parse(packageJson)).toEqual(
44+
expect.objectContaining({
45+
scripts: expect.objectContaining({
46+
"should-semantic-release": "should-semantic-release --verbose",
47+
}),
48+
})
49+
);
50+
});
51+
52+
it("includes a test script when unitTests is true", async () => {
53+
mockReadFileAsJson.mockResolvedValue({});
54+
55+
const packageJson = await writePackageJson({
56+
...values,
57+
unitTests: true,
58+
});
59+
60+
expect(JSON.parse(packageJson)).toEqual(
61+
expect.objectContaining({
62+
scripts: expect.objectContaining({
63+
test: "vitest",
64+
}),
65+
})
66+
);
67+
});
68+
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { readFileAsJson } from "../../../../shared/readFileAsJson.js";
2+
import { HydrationInputValues } from "../../../values/types.js";
3+
import { formatJson } from "./formatters/formatJson.js";
4+
5+
export async function writePackageJson({
6+
author,
7+
description,
8+
email,
9+
owner,
10+
releases,
11+
repository,
12+
unitTests,
13+
}: Pick<
14+
HydrationInputValues,
15+
| "author"
16+
| "description"
17+
| "email"
18+
| "owner"
19+
| "releases"
20+
| "repository"
21+
| "unitTests"
22+
>) {
23+
return formatJson({
24+
// To start, copy over all existing package fields (e.g. "dependencies")
25+
...((await readFileAsJson("./package.json")) as object),
26+
27+
// Remove fields we know we don't want, such as old or redundant configs
28+
eslintConfig: undefined,
29+
husky: undefined,
30+
prettierConfig: undefined,
31+
types: undefined,
32+
33+
// The rest of the fields are ones we know from our template
34+
name: repository,
35+
description,
36+
repository: {
37+
type: "git",
38+
url: `https://github.com/${owner}/${repository}`,
39+
},
40+
license: "MIT",
41+
author: { email, name: author },
42+
type: "module",
43+
main: "./lib/index.js",
44+
files: ["lib/", "package.json", "LICENSE.md", "README.md"],
45+
scripts: {
46+
build: "tsc",
47+
format: 'prettier "**/*" --ignore-unknown',
48+
"format:write": "pnpm format --write",
49+
lint: "eslint . --max-warnings 0 --report-unused-disable-directives",
50+
"lint:knip": "knip",
51+
"lint:md":
52+
'markdownlint "**/*.md" ".github/**/*.md" --rules sentences-per-line',
53+
"lint:package": "npmPkgJsonLint .",
54+
"lint:packages": "pnpm dedupe --check",
55+
"lint:spelling": 'cspell "**" ".github/**/*"',
56+
prepare: "husky install",
57+
...(releases && {
58+
"should-semantic-release": "should-semantic-release --verbose",
59+
}),
60+
...(unitTests && { test: "vitest" }),
61+
},
62+
"lint-staged": {
63+
"*": "prettier --ignore-unknown --write",
64+
},
65+
packageManager: "[email protected]",
66+
engines: {
67+
node: ">=18",
68+
},
69+
publishConfig: {
70+
provenance: true,
71+
},
72+
});
73+
}

src/hydrate/steps/writing/writeStructure.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ import { createStructure } from "./creation/index.js";
33
import { writeStructureWorker } from "./writeStructureWorker.js";
44

55
export async function writeStructure(values: HydrationInputValues) {
6-
return await writeStructureWorker(createStructure(values), ".");
6+
return await writeStructureWorker(await createStructure(values), ".");
77
}

src/setup/settings/addOwnerAsAllContributor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import chalk from "chalk";
44
import { $ } from "execa";
55
import prettier from "prettier";
66

7-
import { readFileAsJSON } from "../readFileAsJSON.js";
7+
import { readFileAsJson } from "../../shared/readFileAsJson.js";
88

99
interface GhUserOutput {
1010
login: string;
@@ -34,7 +34,7 @@ export async function addOwnerAsAllContributor(owner: string) {
3434
"tool",
3535
].join(",")}`;
3636

37-
const existingContributors = (await readFileAsJSON(
37+
const existingContributors = (await readFileAsJson(
3838
"./.all-contributorsrc"
3939
)) as AllContributorsData;
4040
if (!isValidAllContributorsData(existingContributors)) {

src/setup/steps/labels/hydrateRepositoryLabels.ts

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

3-
import { readFileAsJSON } from "../../readFileAsJSON.js";
3+
import { readFileAsJson } from "../../../shared/readFileAsJson.js";
44
import { getExistingEquivalentLabel } from "./getExistingEquivalentLabel.js";
55

66
interface GhLabelData {
@@ -22,7 +22,7 @@ export async function hydrateRepositoryLabels() {
2222
) as GhLabelData[]
2323
).map(getLabelName);
2424

25-
const outcomeLabels = (await readFileAsJSON(
25+
const outcomeLabels = (await readFileAsJson(
2626
"./src/setup/labels.json"
2727
)) as FileLabelData[];
2828

src/setup/steps/updateLocalFiles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import replaceInFile from "replace-in-file";
22

3-
import { readFileAsJSON } from "../readFileAsJSON.js";
3+
import { readFileAsJson } from "../../shared/readFileAsJson.js";
44

55
interface UpdateLocalFilesOptions {
66
description: string;
@@ -22,7 +22,7 @@ export async function updateLocalFiles({
2222
repository,
2323
title,
2424
}: UpdateLocalFilesOptions) {
25-
const existingPackage = (await readFileAsJSON(
25+
const existingPackage = (await readFileAsJson(
2626
"./package.json"
2727
)) as ExistingPackageData;
2828

0 commit comments

Comments
 (0)