diff --git a/cspell.json b/cspell.json index 8210d612e..e4ac19ca0 100644 --- a/cspell.json +++ b/cspell.json @@ -2,39 +2,31 @@ "dictionaries": ["typescript"], "ignorePaths": [ "./coverage*", + "./script/__snapshots__", ".github", "CHANGELOG.md", "lib", "node_modules", - "pnpm-lock.yaml", - "./script/__snapshots__" + "pnpm-lock.yaml" ], "words": [ "allcontributors", "apexskier", "arethetypeswrong", - "Codecov", "codespace", - "commitlint", "contributorsrc", - "conventionalcommits", "execa", "infile", - "joshuakgoldberg", "knip", - "lcov", "markdownlintignore", "mtfoley", "npmignore", - "npmjs", "npmpackagejsonlintrc", "outro", "packagejson", "quickstart", "tada", "tsup", - "Unstaged", - "vitest", - "wontfix" + "vitest" ] } diff --git a/script/__snapshots__/migrate-test-e2e.js.snap b/script/__snapshots__/migrate-test-e2e.js.snap index c310762e0..a17b84baf 100644 --- a/script/__snapshots__/migrate-test-e2e.js.snap +++ b/script/__snapshots__/migrate-test-e2e.js.snap @@ -156,42 +156,30 @@ exports[`expected file changes > cspell.json 1`] = ` "dictionaries": ["typescript"], "ignorePaths": [ - "./coverage*", +- "./script/__snapshots__", ".github", "CHANGELOG.md", + "coverage", "lib", "node_modules", -- "pnpm-lock.yaml", -- "./script/__snapshots__" -+ "pnpm-lock.yaml" - ], - "words": [ -- "allcontributors", -- "apexskier", -- "arethetypeswrong", - "Codecov", - "codespace", - "commitlint", + "pnpm-lock.yaml" +@@ ... @@ "contributorsrc", - "conventionalcommits", -- "execa", -- "infile", -- "joshuakgoldberg", + "execa", + "infile", ++ "joshuakgoldberg", "knip", - "lcov", ++ "markdownlint", "markdownlintignore", -- "mtfoley", -- "npmignore", -- "npmjs", - "npmpackagejsonlintrc", - "outro", - "packagejson", + "mtfoley", + "npmignore", +@@ ... @@ "quickstart", -- "tada", + "tada", "tsup", -- "Unstaged", -- "vitest", - "wontfix" +- "vitest" ++ "vitest", ++ "wontfix" ] }" `; diff --git a/script/initialize-test-e2e.js b/script/initialize-test-e2e.js index d0e0e7e91..3f8e414d0 100644 --- a/script/initialize-test-e2e.js +++ b/script/initialize-test-e2e.js @@ -1,7 +1,7 @@ import { $ } from "execa"; import { globby } from "globby"; import { strict as assert } from "node:assert"; -import fs from "node:fs/promises"; +import * as fs from "node:fs/promises"; const description = "New Description Test"; const owner = "RNR1"; diff --git a/script/migrate-test-e2e.js b/script/migrate-test-e2e.js index fed3914fd..018d24718 100644 --- a/script/migrate-test-e2e.js +++ b/script/migrate-test-e2e.js @@ -1,6 +1,6 @@ import chalk from "chalk"; import { $, execaCommand } from "execa"; -import fs from "node:fs/promises"; +import * as fs from "node:fs/promises"; import { assert, describe, expect, test } from "vitest"; import packageData from "../package.json" assert { type: "json" }; diff --git a/src/create/createWithOptions.test.ts b/src/create/createWithOptions.test.ts index 1a5af2cd2..d11b7ad8c 100644 --- a/src/create/createWithOptions.test.ts +++ b/src/create/createWithOptions.test.ts @@ -7,6 +7,7 @@ import { Options } from "../shared/types.js"; import { addToolAllContributors } from "../steps/addToolAllContributors.js"; import { finalizeDependencies } from "../steps/finalizeDependencies.js"; import { initializeGitHubRepository } from "../steps/initializeGitHubRepository/index.js"; +import { populateCSpellDictionary } from "../steps/populateCSpellDictionary.js"; import { runCommands } from "../steps/runCommands.js"; import { createWithOptions } from "./createWithOptions.js"; @@ -60,6 +61,8 @@ vi.mock("../steps/writeReadme/index.js"); vi.mock("../steps/finalizeDependencies.js"); +vi.mock("../steps/populateCSpellDictionary.js"); + vi.mock("../steps/runCommands.js"); vi.mock("../shared/doesRepositoryExist.js", () => ({ @@ -111,7 +114,7 @@ describe("createWithOptions", () => { expect(addToolAllContributors).not.toHaveBeenCalled(); }); - it("does not call finalizeDependencies or runCommands when skipInstall is true", async () => { + it("does not call finalizeDependencies, populateCSpellDictionary, or runCommands when skipInstall is true", async () => { const options = { ...optionsBase, skipInstall: true, @@ -119,10 +122,11 @@ describe("createWithOptions", () => { await createWithOptions({ github, options }); expect(finalizeDependencies).not.toHaveBeenCalled(); + expect(populateCSpellDictionary).not.toHaveBeenCalled(); expect(runCommands).not.toHaveBeenCalled(); }); - it("calls finalizeDependencies and runCommands when skipInstall is false", async () => { + it("calls finalizeDependencies, populateCSpellDictionary, and runCommands when skipInstall is false", async () => { const options = { ...optionsBase, skipInstall: false, @@ -131,6 +135,7 @@ describe("createWithOptions", () => { await createWithOptions({ github, options }); expect(finalizeDependencies).toHaveBeenCalledWith(options); + expect(populateCSpellDictionary).toHaveBeenCalled(); expect(runCommands).toHaveBeenCalled(); }); diff --git a/src/create/createWithOptions.ts b/src/create/createWithOptions.ts index 8c03d0c8d..9351c2064 100644 --- a/src/create/createWithOptions.ts +++ b/src/create/createWithOptions.ts @@ -7,6 +7,7 @@ import { GitHubAndOptions } from "../shared/options/readOptions.js"; import { addToolAllContributors } from "../steps/addToolAllContributors.js"; import { finalizeDependencies } from "../steps/finalizeDependencies.js"; import { initializeGitHubRepository } from "../steps/initializeGitHubRepository/index.js"; +import { populateCSpellDictionary } from "../steps/populateCSpellDictionary.js"; import { runCommands } from "../steps/runCommands.js"; import { writeReadme } from "../steps/writeReadme/index.js"; import { writeStructure } from "../steps/writing/writeStructure.js"; @@ -38,6 +39,13 @@ export async function createWithOptions({ github, options }: GitHubAndOptions) { finalizeDependencies(options), ); + if (!options.excludeLintSpelling) { + await withSpinner( + "Populating CSpell dictionary", + populateCSpellDictionary, + ); + } + await runCommands( "Cleaning up files", createCleanUpFilesCommands({ diff --git a/src/migrate/migrateWithOptions.ts b/src/migrate/migrateWithOptions.ts index 04b769565..05b128ace 100644 --- a/src/migrate/migrateWithOptions.ts +++ b/src/migrate/migrateWithOptions.ts @@ -5,6 +5,7 @@ import { clearUnnecessaryFiles } from "../steps/clearUnnecessaryFiles.js"; import { detectExistingContributors } from "../steps/detectExistingContributors.js"; import { finalizeDependencies } from "../steps/finalizeDependencies.js"; import { initializeGitHubRepository } from "../steps/initializeGitHubRepository/index.js"; +import { populateCSpellDictionary } from "../steps/populateCSpellDictionary.js"; import { runCommands } from "../steps/runCommands.js"; import { updateAllContributorsTable } from "../steps/updateAllContributorsTable.js"; import { updateLocalFiles } from "../steps/updateLocalFiles.js"; @@ -61,6 +62,10 @@ export async function migrateWithOptions({ ); } + if (!options.excludeLintSpelling) { + await withSpinner("Populating CSpell dictionary", populateCSpellDictionary); + } + await runCommands( "Cleaning up files", createCleanUpFilesCommands({ diff --git a/src/shared/options/createOptionDefaults/index.ts b/src/shared/options/createOptionDefaults/index.ts index e50cf741f..a4f825a01 100644 --- a/src/shared/options/createOptionDefaults/index.ts +++ b/src/shared/options/createOptionDefaults/index.ts @@ -2,7 +2,7 @@ import { $ } from "execa"; import gitRemoteOriginUrl from "git-remote-origin-url"; import gitUrlParse from "git-url-parse"; import lazyValue from "lazy-value"; -import fs from "node:fs/promises"; +import * as fs from "node:fs/promises"; import npmUser from "npm-user"; import { readPackageData } from "../../packages.js"; diff --git a/src/shared/readFileSafe.test.ts b/src/shared/readFileSafe.test.ts index c910ca464..2722a19c7 100644 --- a/src/shared/readFileSafe.test.ts +++ b/src/shared/readFileSafe.test.ts @@ -5,12 +5,8 @@ import { readFileSafe } from "./readFileSafe.js"; const mockReadFile = vi.fn(); vi.mock("node:fs/promises", () => ({ - get default() { - return { - get readFile() { - return mockReadFile; - }, - }; + get readFile() { + return mockReadFile; }, })); diff --git a/src/shared/readFileSafe.ts b/src/shared/readFileSafe.ts index 046c4b061..652a25c1f 100644 --- a/src/shared/readFileSafe.ts +++ b/src/shared/readFileSafe.ts @@ -1,4 +1,4 @@ -import fs from "node:fs/promises"; +import * as fs from "node:fs/promises"; export async function readFileSafe(filePath: URL | string, fallback: string) { try { diff --git a/src/steps/addOwnerAsAllContributor.test.ts b/src/steps/addOwnerAsAllContributor.test.ts index 18f419bc1..ba62f85dd 100644 --- a/src/steps/addOwnerAsAllContributor.test.ts +++ b/src/steps/addOwnerAsAllContributor.test.ts @@ -1,7 +1,7 @@ -import prettier from "prettier"; import { describe, expect, it, vi } from "vitest"; import { addOwnerAsAllContributor } from "./addOwnerAsAllContributor.js"; +import { formatJson } from "./writing/creation/formatters/formatJson.js"; const mock$ = vi.fn(); @@ -14,12 +14,8 @@ vi.mock("execa", () => ({ const mockWriteFile = vi.fn(); vi.mock("node:fs/promises", () => ({ - get default() { - return { - get writeFile() { - return mockWriteFile; - }, - }; + get writeFile() { + return mockWriteFile; }, })); @@ -76,12 +72,9 @@ describe("addOwnerAsAllContributor", () => { expect(mockWriteFile).toHaveBeenCalledWith( "./.all-contributorsrc", - await prettier.format( - JSON.stringify({ - contributors: [{ contributions: ["tool"], login: mockOwner }], - }), - { parser: "json" }, - ), + await formatJson({ + contributors: [{ contributions: ["tool"], login: mockOwner }], + }), ); }); @@ -100,15 +93,12 @@ describe("addOwnerAsAllContributor", () => { expect(mockWriteFile).toHaveBeenCalledWith( "./.all-contributorsrc", - await prettier.format( - JSON.stringify({ - contributors: [ - { contributions: ["bug", "fix", "tool"], login: mockOwner }, - { contributions: ["tool"], login: "JoshuaKGoldberg" }, - ], - }), - { parser: "json" }, - ), + await formatJson({ + contributors: [ + { contributions: ["bug", "fix", "tool"], login: mockOwner }, + { contributions: ["tool"], login: "JoshuaKGoldberg" }, + ], + }), ); }); }); diff --git a/src/steps/addOwnerAsAllContributor.ts b/src/steps/addOwnerAsAllContributor.ts index 6a3b68622..b4a7e12d3 100644 --- a/src/steps/addOwnerAsAllContributor.ts +++ b/src/steps/addOwnerAsAllContributor.ts @@ -1,9 +1,9 @@ -import fs from "node:fs/promises"; -import prettier from "prettier"; +import * as fs from "node:fs/promises"; import { getGitHubUserAsAllContributor } from "../shared/getGitHubUserAsAllContributor.js"; import { readFileAsJson } from "../shared/readFileAsJson.js"; import { AllContributorsData, Options } from "../shared/types.js"; +import { formatJson } from "./writing/creation/formatters/formatJson.js"; export async function addOwnerAsAllContributor( options: Pick, @@ -41,13 +41,10 @@ export async function addOwnerAsAllContributor( await fs.writeFile( "./.all-contributorsrc", - await prettier.format( - JSON.stringify({ - ...existingContributors, - contributors, - }), - { parser: "json" }, - ), + await formatJson({ + ...existingContributors, + contributors, + }), ); } diff --git a/src/steps/clearChangelog.ts b/src/steps/clearChangelog.ts index d543f6d02..f2641cf12 100644 --- a/src/steps/clearChangelog.ts +++ b/src/steps/clearChangelog.ts @@ -1,4 +1,4 @@ -import fs from "node:fs/promises"; +import * as fs from "node:fs/promises"; import prettier from "prettier"; export async function clearChangelog() { diff --git a/src/steps/clearUnnecessaryFiles.ts b/src/steps/clearUnnecessaryFiles.ts index 7f03fe3d3..80878ced1 100644 --- a/src/steps/clearUnnecessaryFiles.ts +++ b/src/steps/clearUnnecessaryFiles.ts @@ -1,4 +1,4 @@ -import fs from "node:fs/promises"; +import * as fs from "node:fs/promises"; const globPaths = [ ...extensions(".babelrc", "cjs", "cts", "js", "json", "mjs"), diff --git a/src/steps/populateCSpellDictionary.test.ts b/src/steps/populateCSpellDictionary.test.ts new file mode 100644 index 000000000..0d50a6aa9 --- /dev/null +++ b/src/steps/populateCSpellDictionary.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it, vi } from "vitest"; + +import { populateCSpellDictionary } from "./populateCSpellDictionary.js"; + +const mock$ = vi.fn(); + +vi.mock("execa", () => ({ + get $() { + return mock$; + }, +})); + +const mockReadFile = vi.fn(); +const mockWriteFile = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get readFile() { + return mockReadFile; + }, + get writeFile() { + return mockWriteFile; + }, +})); + +const mockFormatJson = vi.fn(); + +vi.mock("./writing/creation/formatters/formatJson.js", () => ({ + get formatJson() { + return mockFormatJson; + }, +})); + +describe("populateCSpellDictionary", () => { + it("works with no existing words when the existing cspell.json has no words", async () => { + const unknownWords = ["abc"]; + + mock$.mockResolvedValue({ + stdout: ` + file-1.ts Unknown word (${unknownWords[0]}) + `, + }); + + mockReadFile.mockResolvedValue(JSON.stringify({})); + + await populateCSpellDictionary(); + + expect(mockFormatJson.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "words": [ + "abc", + ], + }, + ], + ] + `); + }); + + it("adds unknown words to cspell.json", async () => { + const existingWords = ["abc", "ghi", "casing"]; + const unknownWords = ["def", "jkl", "Casing"]; + + mock$.mockResolvedValue({ + stdout: ` + file-1.ts Unknown word (${unknownWords[0]}) + file-2.ts Unknown word (${unknownWords[1]}) + file-2.ts Unknown word (${unknownWords[0]}) + `, + }); + + mockReadFile.mockResolvedValue(JSON.stringify({ words: existingWords })); + + await populateCSpellDictionary(); + + expect(mockFormatJson.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "words": [ + "abc", + "casing", + "def", + "ghi", + "jkl", + ], + }, + ], + ] + `); + }); + + it("doesn't add an upper-cased word when its lower-case will also be in the dictionary", async () => { + const existingWords = ["existing"]; + const unknownWords = ["abc", "Abc"]; + + mock$.mockResolvedValue({ + stdout: ` + file-1.ts Unknown word (${unknownWords[0]}) + file-2.ts Unknown word (${unknownWords[1]}) + `, + }); + + mockReadFile.mockResolvedValue(JSON.stringify({ words: existingWords })); + + await populateCSpellDictionary(); + + expect(mockFormatJson.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "words": [ + "abc", + "existing", + ], + }, + ], + ] + `); + }); +}); diff --git a/src/steps/populateCSpellDictionary.ts b/src/steps/populateCSpellDictionary.ts new file mode 100644 index 000000000..8633bec44 --- /dev/null +++ b/src/steps/populateCSpellDictionary.ts @@ -0,0 +1,42 @@ +import { $ } from "execa"; +import * as fs from "node:fs/promises"; + +import { readFileAsJson } from "../shared/readFileAsJson.js"; +import { formatJson } from "./writing/creation/formatters/formatJson.js"; + +async function getStdout() { + try { + return await $`pnpm run lint:spelling`; + } catch (error) { + return error as { stdout: string }; + } +} + +export async function populateCSpellDictionary() { + const { stdout } = await getStdout(); + const unknownWords = new Set( + Array.from(stdout.matchAll(/Unknown word \((.+)\)/g)).map( + ([, matched]) => matched, + ), + ); + + const existing = (await readFileAsJson("cspell.json")) as Partial< + typeof import("../../cspell.json") + >; + + const allWords = [...(existing.words ?? []), ...unknownWords]; + const allWordsUnique = new Set(allWords); + + await fs.writeFile( + "cspell.json", + await formatJson({ + ...existing, + words: allWords + .filter((word) => { + const wordLowerCase = word.toLowerCase(); + return word === wordLowerCase || !allWordsUnique.has(wordLowerCase); + }) + .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())), + }), + ); +} diff --git a/src/steps/removeSetupScripts.ts b/src/steps/removeSetupScripts.ts index 5ef904986..8a1e2515f 100644 --- a/src/steps/removeSetupScripts.ts +++ b/src/steps/removeSetupScripts.ts @@ -1,4 +1,4 @@ -import fs from "node:fs/promises"; +import * as fs from "node:fs/promises"; const globPaths = [ "./bin", diff --git a/src/steps/updateAllContributorsTable.ts b/src/steps/updateAllContributorsTable.ts index 5755b1a82..47e51cdda 100644 --- a/src/steps/updateAllContributorsTable.ts +++ b/src/steps/updateAllContributorsTable.ts @@ -1,9 +1,9 @@ import { $ } from "execa"; -import fs from "node:fs/promises"; -import prettier from "prettier"; +import * as fs from "node:fs/promises"; import { readFileSafeAsJson } from "../shared/readFileSafeAsJson.js"; import { AllContributorsData, Options } from "../shared/types.js"; +import { formatJson } from "./writing/creation/formatters/formatJson.js"; export async function updateAllContributorsTable({ owner, @@ -11,16 +11,13 @@ export async function updateAllContributorsTable({ }: Pick) { await fs.writeFile( ".all-contributorsrc", - await prettier.format( - JSON.stringify({ - ...((await readFileSafeAsJson( - ".all-contributorsrc", - )) as AllContributorsData), - projectName: repository, - projectOwner: owner, - }), - { parser: "json" }, - ), + await formatJson({ + ...((await readFileSafeAsJson( + ".all-contributorsrc", + )) as AllContributorsData), + projectName: repository, + projectOwner: owner, + }), ); await $`npx -y all-contributors-cli generate`; diff --git a/src/steps/updateReadme.test.ts b/src/steps/updateReadme.test.ts index 8cd24333c..135c45629 100644 --- a/src/steps/updateReadme.test.ts +++ b/src/steps/updateReadme.test.ts @@ -5,10 +5,8 @@ import { updateReadme } from "./updateReadme.js"; const mockWriteFile = vi.fn(); vi.mock("node:fs/promises", () => ({ - default: { - get writeFile() { - return mockWriteFile; - }, + get writeFile() { + return mockWriteFile; }, })); diff --git a/src/steps/updateReadme.ts b/src/steps/updateReadme.ts index e5b5c404d..60320202d 100644 --- a/src/steps/updateReadme.ts +++ b/src/steps/updateReadme.ts @@ -1,4 +1,4 @@ -import fs from "node:fs/promises"; +import * as fs from "node:fs/promises"; import { EOL } from "node:os"; import { readFileSafe } from "../shared/readFileSafe.js"; diff --git a/src/steps/writeReadme/index.test.ts b/src/steps/writeReadme/index.test.ts index bab3f61c2..22c45587f 100644 --- a/src/steps/writeReadme/index.test.ts +++ b/src/steps/writeReadme/index.test.ts @@ -6,12 +6,8 @@ import { writeReadme } from "./index.js"; const mockWriteFile = vi.fn(); vi.mock("node:fs/promises", () => ({ - get default() { - return { - get writeFile() { - return mockWriteFile; - }, - }; + get writeFile() { + return mockWriteFile; }, })); diff --git a/src/steps/writeReadme/index.ts b/src/steps/writeReadme/index.ts index 709a11822..85478b540 100644 --- a/src/steps/writeReadme/index.ts +++ b/src/steps/writeReadme/index.ts @@ -1,4 +1,4 @@ -import fs from "node:fs/promises"; +import * as fs from "node:fs/promises"; import { readFileSafe } from "../../shared/readFileSafe.js"; import { Options } from "../../shared/types.js"; diff --git a/src/steps/writing/creation/formatters/formatJson.ts b/src/steps/writing/creation/formatters/formatJson.ts index d80b66cea..e35f8eb27 100644 --- a/src/steps/writing/creation/formatters/formatJson.ts +++ b/src/steps/writing/creation/formatters/formatJson.ts @@ -9,6 +9,7 @@ export async function formatJson(value: object) { ), { parser: "json", + useTabs: true, }, ); } diff --git a/src/steps/writing/creation/rootFiles.ts b/src/steps/writing/creation/rootFiles.ts index a2d002076..d9146b333 100644 --- a/src/steps/writing/creation/rootFiles.ts +++ b/src/steps/writing/creation/rootFiles.ts @@ -112,22 +112,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "node_modules", "pnpm-lock.yaml", ], - words: [ - "Codecov", - "codespace", - "commitlint", - "contributorsrc", - "conventionalcommits", - ...(options.excludeLintKnip ? [] : ["knip"]), - "lcov", - "markdownlintignore", - "npmpackagejsonlintrc", - "outro", - "packagejson", - "tsup", - "quickstart", - "wontfix", - ].sort(), }), }), ...(!options.excludeLintKnip && {