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; 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/__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/__mocks__/fs.js b/src/__mocks__/fs.js similarity index 100% rename from __mocks__/fs.js rename to src/__mocks__/fs.js diff --git a/src/__tests__/generatePluginFiles.spec.ts b/src/__tests__/generatePluginFiles.spec.ts index 4dc46c6..6534c44 100644 --- a/src/__tests__/generatePluginFiles.spec.ts +++ b/src/__tests__/generatePluginFiles.spec.ts @@ -1,4 +1,18 @@ -import { transformFileContents, transformFileName } from "~/generatePluginFiles"; +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 })); + afterEach(() => vol.reset()); + + return ufs + .use(jest.requireActual('fs')) + .use(createFsFromVolume(vol) as any); +}); describe("transformFileName", () => { const transforms = { "REPLACE_ME": "replaced" }; @@ -37,3 +51,50 @@ describe("transformFileContents", () => { expect(result.split("\n").length).toBe(newlineSplits); }); }); + +describe("generatePluginFiles", () => { + 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"); + }); +}); 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; diff --git a/src/index.ts b/src/index.ts index be7bc94..8c6be00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,34 @@ renderMasthead(); renderCliInfo(); console.log(); -const generateProject = new Promise(async (resolve, reject) => { +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() { cancelled = true; @@ -25,19 +52,12 @@ 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"); 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!`); @@ -55,44 +75,21 @@ 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); } } console.timeEnd("Done in"); - return resolve(true); + return resolve(); }); generateProject .catch(err => { - error(err.message) + err instanceof Error ? error(err.message) : error(err); if (!IS_PRODUCTION) console.error(err); }) .finally(() => renderGoodbye()); 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));