cli is where the scripts are executed when the user runs a CLI command
docs is the folder that houses markdown files
scripts is the folder that houses scripts, containing the logic of the operations
templates is the folder that houses templates for the new files
Note: The separation of cli and scripts enables us to extract and write unit tests for the core logic, without having to worry about the execution context (via a command line or via an outer function call).
Implementation
Creating a template
First of, we need to create a new template in the templates folder:
// templates/gen-doc.tsexport const genDocTemplate = (title: string) => `# ${title}Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`
When the user runs npm run gen-doc, the ./cli/gen-doc.ts file will be executed.
Adding a script for the file gen operation
Within scripts, we create a gen-doc.ts file and define a genDoc function that handles the file generation. The function accepts fileName as an argument, then transforms and passes it to the template.
// scripts/gen-doc.tsimport fs from 'fs'import path from 'path'import { genDocTemplate } from '../templates/gen-doc'const DOCS_DIR = path.join(process.cwd(), 'docs')export const genDoc = (fileName?: string) => { // `fileName` can be `undefined` if the user runs // the `gen-doc` command without specifying a file name if (!fileName) { throw new Error('You must include a file name. Example: my-doc.') } // Throw an error if the file already exists if (fs.existsSync(`${DOCS_DIR}/${fileName}.md`)) { throw new Error('A file with that name already exists.') } // Transform `fileName` to a title, // with hyphens removed and the first character capitalized const title = `${fileName.charAt(0).toUpperCase() + fileName.slice(1)}` .split('-') .join(' ') // Create a new doc file fs.writeFileSync(`${DOCS_DIR}/${fileName}.md`, genDocTemplate(title)) console.log(`${fileName}.md created successfully.`)}
Handling the gen-doc command
The last step is to create a genDoc.ts file in the cli folder. When the user runs npm run gen-doc [fileName], this file is executed, extracts the provided fileName, and calls the genDoc function from the script:
// cli/gen-doc.tsimport { genDoc } from '../scripts/gen-doc'// Grab file name from terminal argumentconst fileName = process.argv[2]genDoc(fileName)
Testing
Manual testing
We can test the CLI by running the following command in the terminal:
npm run gen-doc my-doc
A new my-doc.md file should be created in the docs folder.
Unit testing
As previously mentioned, we are going to write tests for the core logic, which is the genDoc function in script/gen-doc.ts. The fs module is mocked with memfs to prevent the tests from writing to the real file system.
Within the scripts folder, create a gen-doc.test.ts file as follows:
// scripts/gen-doc.test.tsimport { describe, it, vi, expect, beforeEach, afterEach } from 'vitest'import fs from 'fs'import { vol } from 'memfs'import path from 'path'import { genDoc } from './gen-doc'const removeWhitespace = (str: string) => str.replace(/\s+/g, ' ')vi.mock('fs', async () => { const memfs: { fs: typeof fs } = await vi.importActual('memfs') return { default: memfs.fs, }})describe('genDoc', () => { beforeEach(() => { // Create a `docs` folder with a `first-doc.md` in the virtual file system vol.fromJSON({ 'docs/first-doc.md': 'Hello world', }) }) afterEach(() => { vol.reset() }) it('should create a new doc file if the provided file name is valid', () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) genDoc('second-doc') expect(logSpy).toHaveBeenCalledWith('second-doc.md created successfully.') const filePath = path.join(process.cwd(), 'docs/second-doc.md') expect(fs.existsSync(filePath)).toBe(true) const fileContent = fs.readFileSync(filePath, 'utf-8') expect(removeWhitespace(fileContent)).toContain( removeWhitespace( `# Second doc Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.` ) ) })})
In the terminal, run the following command:
npm test -t scripts/gen-doc.test.ts
The test should pass successfully.
Some notes on the test:
By default, Vitest requires importing test APIs individually for explicitness: