From 6eb48db3b5a574c8ef9288640d30d2187512aadf Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Wed, 26 Feb 2025 18:09:21 -0800 Subject: [PATCH 1/9] refactor: rename gitignore transform function; --- src/generatePluginFiles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/generatePluginFiles.ts b/src/generatePluginFiles.ts index 0b15fcb..e4c3bdf 100644 --- a/src/generatePluginFiles.ts +++ b/src/generatePluginFiles.ts @@ -20,7 +20,7 @@ export function transformFileContents(content: string, transforms?: Transforms): // Source: // https://github.com/facebook/create-react-app/blob/main/packages/react-scripts/scripts/init.js -function renameOrOverwriteIfExistsGitignore(targetPath: string) { +function renameAndOverwriteIfExistsGitignore(targetPath: string) { const gitignoreExists = fs.existsSync(path.join(targetPath, 'gitignore')); if (!gitignoreExists) return; @@ -67,7 +67,7 @@ function generatePluginFiles(templatePath: string, targetPath: string, transform generatePluginFiles(originPath, destPath, transforms); } - renameOrOverwriteIfExistsGitignore(targetPath); + renameAndOverwriteIfExistsGitignore(targetPath); } export default generatePluginFiles; From 4c4428cbc1112f65b7a07b9f48c56615ffa26850 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Wed, 26 Feb 2025 19:39:37 -0800 Subject: [PATCH 2/9] test: mock fs with unionfs; --- package-lock.json | 19 ++++++++++++++++++- package.json | 3 ++- src/__tests__/generatePluginFiles.spec.ts | 21 ++++++++++++++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f1e5da..7f96678 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,8 @@ "ts-jest-mock-import-meta": "^1.2.1", "ts-node-dev": "^2.0.0", "tsx": "^4.19.2", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "unionfs": "^4.5.4" } }, "node_modules/@ampproject/remapping": { @@ -2489,6 +2490,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true, + "license": "Unlicense" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5953,6 +5961,15 @@ "dev": true, "license": "MIT" }, + "node_modules/unionfs": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/unionfs/-/unionfs-4.5.4.tgz", + "integrity": "sha512-qI3RvJwwdFcWUdZz1dWgAyLSfGlY2fS2pstvwkZBUTnkxjcnIvzriBLtqJTKz9FtArAvJeiVCqHlxhOw8Syfyw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", diff --git a/package.json b/package.json index 8242619..46d018a 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "ts-jest-mock-import-meta": "^1.2.1", "ts-node-dev": "^2.0.0", "tsx": "^4.19.2", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "unionfs": "^4.5.4" } } diff --git a/src/__tests__/generatePluginFiles.spec.ts b/src/__tests__/generatePluginFiles.spec.ts index 4dc46c6..ad08e5b 100644 --- a/src/__tests__/generatePluginFiles.spec.ts +++ b/src/__tests__/generatePluginFiles.spec.ts @@ -1,4 +1,16 @@ -import { transformFileContents, transformFileName } from "~/generatePluginFiles"; +import path from "path"; +import { vol, createFsFromVolume } from "memfs"; +import { ufs } from "unionfs"; +import generatePluginFiles, { transformFileContents, transformFileName } from "~/generatePluginFiles"; + +jest.mock('fs', () => { + beforeEach(() => vol.mkdirSync(path.join(process.cwd(), '.playground'), { recursive: true })); + afterEach(() => vol.reset()); + + return ufs + .use(jest.requireActual('fs')) + .use(createFsFromVolume(vol) as any); +}); describe("transformFileName", () => { const transforms = { "REPLACE_ME": "replaced" }; @@ -37,3 +49,10 @@ describe("transformFileContents", () => { expect(result.split("\n").length).toBe(newlineSplits); }); }); + +describe("generatePluginFiles", () => { + it("makes a new directory", () => { + generatePluginFiles("templates/standard-plugin", ".playground/newDir", {}); + expect(ufs.existsSync(".playground/newDir")).toBe(true); + }); +}); From f5b6815cdf8978fd9bc44750a8d0005e0e2cdea2 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Wed, 26 Feb 2025 19:40:40 -0800 Subject: [PATCH 3/9] test: move mocks to src; --- {__mocks__ => src/__mocks__}/fs.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {__mocks__ => src/__mocks__}/fs.js (100%) diff --git a/__mocks__/fs.js b/src/__mocks__/fs.js similarity index 100% rename from __mocks__/fs.js rename to src/__mocks__/fs.js From a2b1395f133c384702d544305974fe0f402bca78 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:28:29 -0800 Subject: [PATCH 4/9] test: add tests for generatePluginFiles default; --- src/__fixtures__/mock.md | 7 ++++ src/__fixtures__/templateMocks.ts | 7 ++++ src/__tests__/generatePluginFiles.spec.ts | 46 ++++++++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/__fixtures__/mock.md create mode 100644 src/__fixtures__/templateMocks.ts diff --git a/src/__fixtures__/mock.md b/src/__fixtures__/mock.md new file mode 100644 index 0000000..c5e4bab --- /dev/null +++ b/src/__fixtures__/mock.md @@ -0,0 +1,7 @@ +Voluptate qui rem ut iure recusandae non voluptate. +Consequatur qui quos REPLACE_THIS optio quaerat voluptatem illo velit. +Rerum dolores possimus REPLACE_THIS non dolores. +Veniam sit quam porro rem distinctio omnis cumque suscipit. replace_this + +REPLACE_THIS + diff --git a/src/__fixtures__/templateMocks.ts b/src/__fixtures__/templateMocks.ts new file mode 100644 index 0000000..4697fc1 --- /dev/null +++ b/src/__fixtures__/templateMocks.ts @@ -0,0 +1,7 @@ +import { NestedDirectoryJSON } from "memfs"; + +export const nestedDirectories: NestedDirectoryJSON = { + "nested-directory": { + "another-directory": null + } +} diff --git a/src/__tests__/generatePluginFiles.spec.ts b/src/__tests__/generatePluginFiles.spec.ts index ad08e5b..6534c44 100644 --- a/src/__tests__/generatePluginFiles.spec.ts +++ b/src/__tests__/generatePluginFiles.spec.ts @@ -2,6 +2,8 @@ import path from "path"; import { vol, createFsFromVolume } from "memfs"; import { ufs } from "unionfs"; import generatePluginFiles, { transformFileContents, transformFileName } from "~/generatePluginFiles"; +import { nestedDirectories } from "~/__fixtures__/templateMocks"; +import assert from "assert"; jest.mock('fs', () => { beforeEach(() => vol.mkdirSync(path.join(process.cwd(), '.playground'), { recursive: true })); @@ -51,8 +53,48 @@ describe("transformFileContents", () => { }); describe("generatePluginFiles", () => { - it("makes a new directory", () => { - generatePluginFiles("templates/standard-plugin", ".playground/newDir", {}); + it("makes nested directories", () => { + vol.fromNestedJSON({ 'fixture': nestedDirectories }); + + generatePluginFiles("fixture", ".playground/newDir", {}); + expect(ufs.existsSync(".playground/newDir")).toBe(true); + expect(ufs.existsSync(`.playground/newDir/${Object.keys(nestedDirectories)[0]}`)).toBe(true); + }); + + it("transforms file content", () => { + vol.fromNestedJSON({ + 'fixture': { + ...nestedDirectories, + 'REPLACE_THIS': ufs.readFileSync("src/__fixtures__/mock.md") + } + }); + + generatePluginFiles("fixture", ".playground/newDir", { + "REPLACE_THIS": "TO_THIS" + }); + + assert(ufs.existsSync(".playground/newDir/TO_THIS")); + + const file = ufs.readFileSync(".playground/newDir/TO_THIS", "utf-8") + expect(file).toContain("TO_THIS"); + expect(file).not.toContain("REPLACE_THIS"); + }); + + it("moves gitignore in-place", () => { + vol.fromNestedJSON({ + "fixture": { + "gitignore": "after", + ".gitignore": "before", + } + }); + + generatePluginFiles("fixture", ".playground/newDir", {}); + + const dirContents = ufs.readdirSync(".playground/newDir"); + expect(dirContents).toContain(".gitignore"); + expect(dirContents).not.toContain("gitignore"); + + expect(ufs.readFileSync(".playground/newDir/.gitignore", "utf-8")).toEqual("after"); }); }); From 61c28a16c833638c1841af3702075445756da096 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:33:13 -0800 Subject: [PATCH 5/9] test: collect coverage from all files in src; --- jest.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jest.config.ts b/jest.config.ts index 4e73ee3..f676182 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -27,6 +27,9 @@ const config: JestConfigWithTsJest = { transform: { "^.+\\.[tj]sx?$": ["ts-jest", tsJestTransformOptions], }, + collectCoverageFrom: [ + "src/**" + ], }; export default config; From 083085442ebaccd63cf99bd4aaf5542094f110db Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:33:50 -0800 Subject: [PATCH 6/9] refactor: spinnerFrames returns string array; --- src/vanity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vanity.ts b/src/vanity.ts index 5bc4b1b..b7bf9a0 100644 --- a/src/vanity.ts +++ b/src/vanity.ts @@ -14,7 +14,7 @@ const HEADER = ` ____ _ ____ _ _ _ _ |_| `; -const spinnerFrames = ((): any[] => { +const spinnerFrames = ((): string[] => { const symbols = "⠁⠂⠄⡀⡈⡐⡠⣀⣁⣂⣄⣌⣔⣤⣥⣦⣮⣶⣷⣿⡿⠿⢟⠟⡛⠛⠫⢋⠋⠍⡉⠉⠑⠡⢁".split(""); const chunkLength = Math.round(Math.sqrt(symbols.length)); From 340841e16d8ceb253acb9aedcc49a628a003f9d2 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:51:24 -0800 Subject: [PATCH 7/9] refactor: remove unnecessary pluginName getter function; --- src/index.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index be7bc94..5421200 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,14 +30,7 @@ const generateProject = new Promise(async (resolve, reject) => { console.time("Done in"); const targetPath = path.resolve(TARGET_BASE, answers.containingDirectoryName); - - const pluginName = (() => { - if (!answers.pluginSameName) { - return answers.pluginName; - } - - return path.parse(answers.containingDirectoryName).base; - })(); + const pluginName = answers.pluginName ?? path.parse(answers.containingDirectoryName).base; if (fs.existsSync(targetPath)) { return reject(`Path ${targetPath} already exists!`); From d4a73a3e85695f6852ea04dd28bd098898750c8d Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Wed, 26 Feb 2025 21:13:49 -0800 Subject: [PATCH 8/9] refactor: extract run shell command logic; --- src/index.ts | 60 ++++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5421200..70c1729 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,33 @@ renderMasthead(); renderCliInfo(); console.log(); +const dotnetCommands = [ + 'dotnet new solution', + 'dotnet sln add src', + 'dotnet build', +]; + +async function runShellCommands(commands: string[], targetPath: string) { + for (const command of commands) { + const startTime = performance.now(); + const spinner = createSpinner(`Running '${command}'...`).start(); + + await new Promise((resolve, reject) => { + exec(command, { cwd: targetPath }).on('close', code => { + if (code === 0) return resolve('done'); + return reject(`Could not run command '${command}'`); + }); + }).catch(e => { + spinner.fail(`Command '${command}' failed!`); + throw new Error(e); + }); + + const elapsed = Math.trunc(Math.abs(performance.now() - startTime)); + spinner.succeed(`Ran '${command}' in ${elapsed}ms`); + } +} + + const generateProject = new Promise(async (resolve, reject) => { let cancelled = false; function onCancel() { @@ -48,34 +75,11 @@ const generateProject = new Promise(async (resolve, reject) => { fs.mkdirSync(targetPath, { recursive: true }); generatePluginFiles(templatePath, targetPath, transforms); - const dotnetCommands = [ - 'dotnet new solution', - 'dotnet sln add src', - 'dotnet build', - ]; - if (answers.setupUsingDotnetCli) { - for (const command of dotnetCommands) { - const startTime = performance.now(); - const spinner = createSpinner(`Running '${command}'...`).start(); - - const doneOrError = await new Promise((resolve, reject) => { - exec(command, { cwd: targetPath }).on('close', code => { - if (code === 0) return resolve('done'); - return reject(`Could not run command '${command}'`); - }); - }).catch(e => { - spinner.fail(`Command '${command}' failed!`); - return e; - }); - - if (doneOrError !== 'done') { - error(doneOrError); - return resolve(true); - } - - const elapsed = Math.trunc(Math.abs(performance.now() - startTime)); - spinner.succeed(`Ran '${command}' in ${elapsed}ms`); + try { + await runShellCommands(dotnetCommands, targetPath) + } catch (e) { + return reject(e); } } @@ -85,7 +89,7 @@ const generateProject = new Promise(async (resolve, reject) => { generateProject .catch(err => { - error(err.message) + err instanceof Error ? error(err.message) : error(err); if (!IS_PRODUCTION) console.error(err); }) .finally(() => renderGoodbye()); From 1a1a70b0da1403a5586621846aaa6510411d436d Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Wed, 26 Feb 2025 21:18:11 -0800 Subject: [PATCH 9/9] refactor: entrypoint promise void resolve; --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 70c1729..8c6be00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,7 +42,7 @@ async function runShellCommands(commands: string[], targetPath: string) { } -const generateProject = new Promise(async (resolve, reject) => { +const generateProject = new Promise(async (resolve, reject) => { let cancelled = false; function onCancel() { cancelled = true; @@ -52,7 +52,7 @@ const generateProject = new Promise(async (resolve, reject) => { const answers = await prompts(parameters, { onCancel }); if (cancelled) { warn("Cancelled making a CounterStrikeSharp plugin."); - return resolve(true); + return resolve(); } console.time("Done in"); @@ -84,7 +84,7 @@ const generateProject = new Promise(async (resolve, reject) => { } console.timeEnd("Done in"); - return resolve(true); + return resolve(); }); generateProject