diff --git a/package-lock.json b/package-lock.json index 5474a393..f4b76206 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ }, "devDependencies": { "@types/debug": "^4.1.9", + "@types/ejs": "^3.1.4", "@types/inquirer": "^9.0.4", "@types/minimist": "^1.2.3", "@types/semver": "^7.5.3", @@ -38,6 +39,7 @@ "@yeoman/transform": "^1.2.0", "c8": "^8.0.1", "cpy-cli": "^5.0.0", + "ejs": "^3.1.9", "esmocha": "^1.0.1", "inquirer": "^9.2.11", "jsdoc": "^4.0.2", @@ -2726,9 +2728,9 @@ } }, "node_modules/@types/ejs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.3.tgz", - "integrity": "sha512-mv5T/JI/bu+pbfz1o+TLl1NF0NIBbjS0Vl6Ppz1YY9DkXfzZT0lelXpfS5i3ZS3U/p90it7uERQpBvLYoK8e4A==" + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.4.tgz", + "integrity": "sha512-fnM/NjByiWdSRJRrmGxgqOSAnmOnsvX1QcNYk5TVyIIj+7ZqOKMb9gQa4OIl/lil2w/8TiTWV+nz3q8yqxez/w==" }, "node_modules/@types/eslint": { "version": "8.44.4", diff --git a/package.json b/package.json index 4886fa18..ce5fdf20 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ }, "devDependencies": { "@types/debug": "^4.1.9", + "@types/ejs": "^3.1.4", "@types/inquirer": "^9.0.4", "@types/minimist": "^1.2.3", "@types/semver": "^7.5.3", @@ -80,6 +81,7 @@ "@yeoman/transform": "^1.2.0", "c8": "^8.0.1", "cpy-cli": "^5.0.0", + "ejs": "^3.1.9", "esmocha": "^1.0.1", "inquirer": "^9.2.11", "jsdoc": "^4.0.2", diff --git a/src/actions/fs.ts b/src/actions/fs.ts index fa76dabf..d0f92a85 100644 --- a/src/actions/fs.ts +++ b/src/actions/fs.ts @@ -1,7 +1,6 @@ /* eslint max-params: [1, 5] */ import assert from 'node:assert'; import { type CopyOptions, type MemFsEditor } from 'mem-fs-editor'; -// eslint-disable-next-line import/no-extraneous-dependencies import type { Data as TemplateData, Options as TemplateOptions } from 'ejs'; import type { OverloadParameters, OverloadReturnType } from '../types-utils.js'; import type BaseGenerator from '../generator.js'; diff --git a/test/fs.test.ts b/test/fs.test.ts index 3954af6d..ff360022 100644 --- a/test/fs.test.ts +++ b/test/fs.test.ts @@ -3,7 +3,8 @@ import path from 'node:path'; import { esmocha } from 'esmocha'; // eslint-disable-next-line n/file-extension-in-import import { TestAdapter } from '@yeoman/adapter/testing'; -import { stub as sinonStub } from 'sinon'; +import { stub as sinonStub, type SinonStub } from 'sinon'; +import type { Data as TemplateData } from 'ejs'; import Environment from 'yeoman-environment'; import BaseGenerator from '../src/generator.js'; import Base from './utils.js'; @@ -11,22 +12,94 @@ import Base from './utils.js'; const randomString = () => Math.random().toString(36).slice(7); const createEnv = () => new Environment({ skipInstall: true, adapter: new TestAdapter() }); +// Make copyTpl() call argument indices more readable +const ARG_FROM = 0; +const ARG_TO = 1; +const ARG_DATA = 2; // A.k.a. context +const ARG_TPLSETTINGS = 3; // Template settings +const ARG_COPYSETTINGS = 4; + +type FSOpResult = { + name: string; + first: string; + second?: string; + dest: string; + returnsUndefined?: boolean; +}; + +let testResults: FSOpResult[] = []; +testResults = testResults.concat([ + { name: 'readTemplate', first: 'templatePath', dest: 'read' }, + { + name: 'copyTemplate', + first: 'templatePath', + second: 'destinationPath', + dest: 'copy', + }, + { + name: 'copyTemplateAsync', + first: 'templatePath', + second: 'destinationPath', + dest: 'copyAsync', + }, + { name: 'readDestination', first: 'destinationPath', dest: 'read' }, + { name: 'writeDestination', first: 'destinationPath', dest: 'write' }, + { + name: 'writeDestinationJSON', + first: 'destinationPath', + dest: 'writeJSON', + }, + { name: 'deleteDestination', first: 'destinationPath', dest: 'delete' }, + { + name: 'copyDestination', + first: 'destinationPath', + second: 'destinationPath', + dest: 'copy', + }, + { + name: 'moveDestination', + first: 'destinationPath', + second: 'destinationPath', + dest: 'move', + }, + { name: 'existsDestination', first: 'destinationPath', dest: 'exists' }, + { + name: 'renderTemplate', + first: 'templatePath', + second: 'destinationPath', + dest: 'copyTpl', + returnsUndefined: true, + }, + { + name: 'renderTemplateAsync', + first: 'templatePath', + second: 'destinationPath', + dest: 'copyTplAsync', + }, +]); + +type BaseGenPaths = Record; + describe('generators.Base (actions/fs)', () => { - const baseReturns = { + const baseReturns: BaseGenPaths = { templatePath: `templatePath${randomString()}`, destinationPath: `destinationPath${randomString()}`, }; const configGetAll = { foo: 'bar' }; - let returns; + let returns: Record; before(function () { this.timeout(10_000); - this.gen = new Base({ env: createEnv(), resolved: 'unknown' }); + this.gen = new Base({ env: createEnv(), resolved: 'unknown', help: true }); }); beforeEach(function () { returns = {}; this.base = new BaseGenerator({ namespace: 'foo', help: true, resolved: 'unknown' }); + + // Why not use a sinonStub for this.base.config as is done in #renderTemplate and #renderTemplateAsync below? + // this.base get config is not being tested in any way below. + // @ts-expect-error Config is a string (not a symbol) and we know it exists on this.base https://github.com/DefinitelyTyped/DefinitelyTyped/issues/33173 esmocha.spyOn(this.base, 'config', 'get').mockReturnValue({ getAll() { return configGetAll; @@ -61,55 +134,7 @@ describe('generators.Base (actions/fs)', () => { } }); - for (const operation of [ - { name: 'readTemplate', first: 'templatePath', dest: 'read' }, - { - name: 'copyTemplate', - first: 'templatePath', - second: 'destinationPath', - dest: 'copy', - }, - { - name: 'copyTemplateAsync', - first: 'templatePath', - second: 'destinationPath', - dest: 'copyAsync', - }, - { name: 'readDestination', first: 'destinationPath', dest: 'read' }, - { name: 'writeDestination', first: 'destinationPath', dest: 'write' }, - { - name: 'writeDestinationJSON', - first: 'destinationPath', - dest: 'writeJSON', - }, - { name: 'deleteDestination', first: 'destinationPath', dest: 'delete' }, - { - name: 'copyDestination', - first: 'destinationPath', - second: 'destinationPath', - dest: 'copy', - }, - { - name: 'moveDestination', - first: 'destinationPath', - second: 'destinationPath', - dest: 'move', - }, - { name: 'existsDestination', first: 'destinationPath', dest: 'exists' }, - { - name: 'renderTemplate', - first: 'templatePath', - second: 'destinationPath', - dest: 'copyTpl', - returnsUndefined: true, - }, - { - name: 'renderTemplateAsync', - first: 'templatePath', - second: 'destinationPath', - dest: 'copyTplAsync', - }, - ]) { + for (const operation of testResults) { const passedArg1 = randomString(); const passedArg2 = randomString(); const passedArg3 = {}; @@ -117,21 +142,23 @@ describe('generators.Base (actions/fs)', () => { // eslint-disable-next-line @typescript-eslint/no-loop-func describe(`#${operation.name}`, () => { - let returnValue; - let expectedReturn; - let firstArgumentHandler; - let secondArgumentHandler; + let returnValue: any; + let expectedReturn: string | undefined; + let firstArgumentHandler: SinonStub; + let secondArgumentHandler: SinonStub; beforeEach(async function () { returnValue = await this.base[operation.name](passedArg1, passedArg2, passedArg3, passedArg4); expectedReturn = operation.returnsUndefined ? undefined : returns[operation.dest]; firstArgumentHandler = this.base[operation.first]; - secondArgumentHandler = this.base[operation.second]; + if (operation.second !== undefined && operation.second !== null) { + secondArgumentHandler = this.base[operation.second]; + } }); it('exists on the generator', () => { - assert(Base.prototype[operation.name]); + assert(operation.name in Base.prototype); }); it('returns the correct value', () => { @@ -204,7 +231,7 @@ describe('generators.Base (actions/fs)', () => { assert(copyTpl.calledOnce); const firsCall = copyTpl.getCall(0); - assert.equal(firsCall.args[2], getAllReturn); + assert.equal(firsCall.args[ARG_DATA], getAllReturn); }); it('gets data with path from config', function () { @@ -213,7 +240,7 @@ describe('generators.Base (actions/fs)', () => { assert(copyTpl.calledOnce); const firsCall = copyTpl.getCall(0); - assert.equal(firsCall.args[2], getPathReturn); + assert.equal(firsCall.args[ARG_DATA], getPathReturn); }); it('concatenates source and destination', function () { @@ -226,9 +253,9 @@ describe('generators.Base (actions/fs)', () => { assert(copyTpl.calledOnce); const firsCall = copyTpl.getCall(0); - assert.equal(firsCall.args[0], path.join(...source)); - assert.equal(firsCall.args[1], path.join(...destination)); - assert.equal(firsCall.args[2], data); + assert.equal(firsCall.args[ARG_FROM], path.join(...source)); + assert.equal(firsCall.args[ARG_TO], path.join(...destination)); + assert.equal(firsCall.args[ARG_DATA], data); }); }); @@ -263,7 +290,7 @@ describe('generators.Base (actions/fs)', () => { assert(copyTplAsync.calledOnce); const firsCall = copyTplAsync.getCall(0); - assert.equal(firsCall.args[2], getAllReturn); + assert.equal(firsCall.args[ARG_DATA], getAllReturn); }); it('gets data with path from config', async function () { @@ -272,7 +299,7 @@ describe('generators.Base (actions/fs)', () => { assert(copyTplAsync.calledOnce); const firsCall = copyTplAsync.getCall(0); - assert.equal(firsCall.args[2], getPathReturn); + assert.equal(firsCall.args[ARG_DATA], getPathReturn); }); it('concatenates source and destination', function () { @@ -285,9 +312,9 @@ describe('generators.Base (actions/fs)', () => { assert(copyTplAsync.calledOnce); const firsCall = copyTplAsync.getCall(0); - assert.equal(firsCall.args[0], path.join(...source)); - assert.equal(firsCall.args[1], path.join(...destination)); - assert.equal(firsCall.args[2], data); + assert.equal(firsCall.args[ARG_FROM], path.join(...source)); + assert.equal(firsCall.args[ARG_TO], path.join(...destination)); + assert.equal(firsCall.args[ARG_DATA], data); }); }); @@ -318,9 +345,9 @@ describe('generators.Base (actions/fs)', () => { assert.equal(copyTpl.callCount, 1); const firsCall = copyTpl.getCall(0); - assert.equal(firsCall.args[0], passedArg1); - assert.equal(firsCall.args[1], passedArg1); - assert.equal(firsCall.args[2], data); + assert.equal(firsCall.args[ARG_FROM], passedArg1); + assert.equal(firsCall.args[ARG_TO], passedArg1); + assert.equal(firsCall.args[ARG_DATA], data); }); it('handles more than 1 template', function () { @@ -348,16 +375,16 @@ describe('generators.Base (actions/fs)', () => { assert.equal(copyTpl.callCount, 2); const firsCall = copyTpl.getCall(0); - assert.equal(firsCall.args[0], passedArg1); - assert.equal(firsCall.args[1], passedArg1); - assert.equal(firsCall.args[2], data); + assert.equal(firsCall.args[ARG_FROM], passedArg1); + assert.equal(firsCall.args[ARG_TO], passedArg1); + assert.equal(firsCall.args[ARG_DATA], data); const secondCall = copyTpl.getCall(1); - assert.equal(secondCall.args[0], secondCallArg1); - assert.equal(secondCall.args[1], secondCallArg2); - assert.equal(secondCall.args[2], data); - assert.equal(secondCall.args[3].foo, templateOptions.foo); - assert.equal(secondCall.args[4], copyOptions); + assert.equal(secondCall.args[ARG_FROM], secondCallArg1); + assert.equal(secondCall.args[ARG_TO], secondCallArg2); + assert.equal(secondCall.args[ARG_DATA], data); + assert.equal(secondCall.args[ARG_TPLSETTINGS].foo, templateOptions.foo); + assert.equal(secondCall.args[ARG_COPYSETTINGS], copyOptions); }); it('skips templates based on when callback', function () { @@ -386,21 +413,21 @@ describe('generators.Base (actions/fs)', () => { assert.equal(copyTpl.callCount, 1); const firsCall = copyTpl.getCall(0); - assert.equal(firsCall.args[0], passedArg1); - assert.equal(firsCall.args[1], passedArg1); - assert.equal(firsCall.args[2], data); + assert.equal(firsCall.args[ARG_FROM], passedArg1); + assert.equal(firsCall.args[ARG_TO], passedArg1); + assert.equal(firsCall.args[ARG_DATA], data); }); it('passes the data to when callback', function () { const passedArg1 = 'foo'; - const templateData = {}; - let receivedData; + const templateData: TemplateData = {}; + let receivedData: TemplateData = { name: 'original value' }; // Set this to something so TypeScript doesn't complain that it is used before set this.gen.renderTemplates( [ { source: passedArg1, - when(data) { + when(data: TemplateData) { receivedData = data; }, }, @@ -442,9 +469,9 @@ describe('generators.Base (actions/fs)', () => { assert.equal(copyTplAsync.callCount, 1); const firsCall = copyTplAsync.getCall(0); - assert.equal(firsCall.args[0], passedArg1); - assert.equal(firsCall.args[1], passedArg1); - assert.equal(firsCall.args[2], data); + assert.equal(firsCall.args[ARG_FROM], passedArg1); + assert.equal(firsCall.args[ARG_TO], passedArg1); + assert.equal(firsCall.args[ARG_DATA], data); }); it('handles more than 1 template', function () { @@ -472,16 +499,16 @@ describe('generators.Base (actions/fs)', () => { assert.equal(copyTplAsync.callCount, 2); const firsCall = copyTplAsync.getCall(0); - assert.equal(firsCall.args[0], passedArg1); - assert.equal(firsCall.args[1], passedArg1); - assert.equal(firsCall.args[2], data); + assert.equal(firsCall.args[ARG_FROM], passedArg1); + assert.equal(firsCall.args[ARG_TO], passedArg1); + assert.equal(firsCall.args[ARG_DATA], data); const secondCall = copyTplAsync.getCall(1); - assert.equal(secondCall.args[0], secondCallArg1); - assert.equal(secondCall.args[1], secondCallArg2); - assert.equal(secondCall.args[2], data); - assert.equal(secondCall.args[3].foo, templateOptions.foo); - assert.equal(secondCall.args[4], copyOptions); + assert.equal(secondCall.args[ARG_FROM], secondCallArg1); + assert.equal(secondCall.args[ARG_TO], secondCallArg2); + assert.equal(secondCall.args[ARG_DATA], data); + assert.equal(secondCall.args[ARG_TPLSETTINGS].foo, templateOptions.foo); + assert.equal(secondCall.args[ARG_COPYSETTINGS], copyOptions); }); it('skips templates based on when callback', async function () { @@ -510,21 +537,21 @@ describe('generators.Base (actions/fs)', () => { assert.equal(copyTplAsync.callCount, 1); const firsCall = copyTplAsync.getCall(0); - assert.equal(firsCall.args[0], passedArg1); - assert.equal(firsCall.args[1], passedArg1); - assert.equal(firsCall.args[2], data); + assert.equal(firsCall.args[ARG_FROM], passedArg1); + assert.equal(firsCall.args[ARG_TO], passedArg1); + assert.equal(firsCall.args[ARG_DATA], data); }); it('passes the data to when callback', function () { const passedArg1 = 'foo'; - const templateData = {}; - let receivedData; + const templateData: TemplateData = {}; + let receivedData: TemplateData = { name: 'original value' }; // Set this to something so TypeScript doesn't complain that it is used before set this.gen.renderTemplatesAsync( [ { source: passedArg1, - when(data) { + when(data: TemplateData) { receivedData = data; }, },