Skip to content

Commit

Permalink
feat(getTemplateOptions): add lifecycle methods (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
10xLaCroixDrinker authored Mar 21, 2022
1 parent 8cb890b commit 741e33a
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 19 deletions.
2 changes: 2 additions & 0 deletions .spelling
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# one word per line, to define a file override use ' - filename'
# where filename is relative to this configuration file

lifecycle
repo
- CODE_OF_CONDUCT.md
Github
neuro
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ A template is any npm package whose default export follows the below API.
module.exports = {
getTemplatePaths,
getTemplateOptions,
getTempalateBanner,
getTemplateBanner,
};
```

Expand Down Expand Up @@ -130,6 +130,19 @@ These values allow you to configure the generator.
* `initialCommitOptions`: Array of additional git options to be passed to the initial commit.
* `storeResponses`: If this is set to `true`, create-using-template will store any responses for your template for future template generations.

##### `lifecycle` object<function>, optional

These functions will run at specific stages of the generation lifecycle. `pre` functions can return `{ skip: true }` to skip that lifecycle stage. The generation stage cannot be skipped. If a lifecycle stage is skipped, its `post` function will not be called.

* `preGenerate`: runs before the module is generated. Generation cannot be skipped.
* `postGenerate`: runs after the module is generated.
* `preGitInit`: runs before initializing the git repo. Skipping this stage will also skip the commit stage, including `preCommit`.
* `postGitInit`: runs after initializing the git repo.
* `preInstall`: runs before installing the module.
* `postInstall`: runs after install.
* `preCommit`: runs before creating the initial commit.
* `postCommit`: runs after creating the initial commit.

##### `dynamicFileNames` object<string, string>, optional
When the generator is ready to write a file to the users project, it will first check this object for a key matching the fileName it is to use. If the key is present, it will instead use the value against that key as the file name.

Expand Down Expand Up @@ -187,7 +200,7 @@ If this function is not exported, no string will be rendered

## 🏆 Contributing

We welcome Your interest in the American Express Open Source Community on Github. Any Contributor to
We welcome Your interest in the American Express Open Source Community on GitHub. Any Contributor to
any Open Source Project managed by the American Express Open Source Community must accept and sign
an Agreement indicating agreement to the terms below. Except for the rights granted in this
Agreement to American Express and to recipients of software distributed by American Express, You
Expand Down
98 changes: 87 additions & 11 deletions __tests__/generate-from-template.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* under the License.
*/

const { getTemplateOptions } = require('ejs'); // see the comment above the ejs mock as to why 'ejs' is our 'template' for this tests
const { getTemplateOptions, lifecycleMocks } = require('ejs'); // see the comment above the ejs mock as to why 'ejs' is our 'template' for this tests
const { Store } = require('data-store');
const path = require('path');
const log = require('../src/utils/log');
Expand Down Expand Up @@ -44,16 +44,31 @@ jest.mock('../src/utils/git', () => ({
// So the jest hack work around to this is to use a package that is installed, but is never imported
// We can mock that package, then use that as our 'template' for the rest of the tests.
// ejs will do, as it is not imported from generate-from-template
jest.mock('ejs', () => ({
getTemplateOptions: jest.fn(() => ({
templateValues: { projectName: 'projectNameMock' },
dynamicFileNames: 'dynamicFileNamesMock',
ignoredFileNames: 'ignoredFileNamesMock',
dynamicDirectoryNames: { dynamicDirectoryName: 'dynamicDirectoryNameRename' },
ignoredDirectories: [],
})),
getTemplatePaths: jest.fn(() => ['path1Mock', 'path2Mock']),
}));
jest.mock('ejs', () => {
const lifecycleMockFns = {
preGenerate: jest.fn(),
postGenerate: jest.fn(),
preGitInit: jest.fn(),
postGitInit: jest.fn(),
preInstall: jest.fn(),
postInstall: jest.fn(),
preCommit: jest.fn(),
postCommit: jest.fn(),
};

return {
getTemplateOptions: jest.fn(() => ({
templateValues: { projectName: 'projectNameMock' },
dynamicFileNames: 'dynamicFileNamesMock',
ignoredFileNames: 'ignoredFileNamesMock',
dynamicDirectoryNames: { dynamicDirectoryName: 'dynamicDirectoryNameRename' },
ignoredDirectories: [],
lifecycle: lifecycleMockFns,
})),
getTemplatePaths: jest.fn(() => ['path1Mock', 'path2Mock']),
lifecycleMocks: lifecycleMockFns,
};
});

describe('generateFromTemplate', () => {
let templatePackage;
Expand All @@ -64,6 +79,7 @@ describe('generateFromTemplate', () => {
templatePackage = require('ejs');
jest.clearAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(Store.prototype, 'get').mockImplementation(getMock);
jest.spyOn(Store.prototype, 'set').mockImplementation(setMock);
});
Expand Down Expand Up @@ -146,6 +162,7 @@ describe('generateFromTemplate', () => {
it('should call walk template for each path', async () => {
await generateFromTemplate({ templateName: '[email protected]' });

expect(lifecycleMocks.preGenerate).toHaveBeenCalledTimes(1);
expect(walkTemplate).toHaveBeenCalledTimes(2);
expect(walkTemplate).toHaveBeenNthCalledWith(1, 'path1Mock', './projectNameMock', {
templateValues: { projectName: 'projectNameMock' },
Expand All @@ -162,30 +179,89 @@ describe('generateFromTemplate', () => {
expect(renameDirectories).toHaveBeenNthCalledWith(1, path.resolve('./projectNameMock'), {
dynamicDirectoryNames: { dynamicDirectoryName: 'dynamicDirectoryNameRename' },
});
expect(lifecycleMocks.postGenerate).toHaveBeenCalledTimes(1);
});

it('should ignore attempts to skip generation & log a warning', async () => {
lifecycleMocks.preGenerate.mockReturnValueOnce({ skip: true });

await generateFromTemplate({ templateName: '[email protected]' });

expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledWith('Cannot skip generation. Ignoring.');
expect(lifecycleMocks.preGenerate).toHaveBeenCalledTimes(1);
expect(walkTemplate).toHaveBeenCalledTimes(2);
expect(lifecycleMocks.postGenerate).toHaveBeenCalledTimes(1);
});

// Step 4
it('should initialize the git repo', async () => {
await generateFromTemplate({ templateName: '[email protected]' });

expect(lifecycleMocks.preGitInit).toHaveBeenCalledTimes(1);
expect(initializeGitRepo).toHaveBeenCalledTimes(1);
expect(initializeGitRepo).toHaveBeenNthCalledWith(1, './projectNameMock');
expect(lifecycleMocks.postGitInit).toHaveBeenCalledTimes(1);
});

it('should not initialize the git repo if the lifecycle method indicates so', async () => {
lifecycleMocks.preGitInit.mockReturnValueOnce({ skip: true });

await generateFromTemplate({ templateName: '[email protected]' });

expect(lifecycleMocks.preGitInit).toHaveBeenCalledTimes(1);
expect(initializeGitRepo).not.toHaveBeenCalled();
expect(lifecycleMocks.postGitInit).not.toHaveBeenCalled();
});

// Step 5
it('should install the module', async () => {
await generateFromTemplate({ templateName: '[email protected]' });

expect(lifecycleMocks.preInstall).toHaveBeenCalledTimes(1);
expect(installModule).toHaveBeenCalledTimes(1);
expect(installModule).toHaveBeenNthCalledWith(1, './projectNameMock');
expect(lifecycleMocks.postInstall).toHaveBeenCalledTimes(1);
});

it('should not install the module if the lifecycle method indicates so', async () => {
lifecycleMocks.preInstall.mockReturnValueOnce({ skip: true });

await generateFromTemplate({ templateName: '[email protected]' });

expect(lifecycleMocks.preInstall).toHaveBeenCalledTimes(1);
expect(installModule).not.toHaveBeenCalled();
expect(lifecycleMocks.postInstall).not.toHaveBeenCalled();
});

// Step 6
it('creates initial commit', async () => {
await generateFromTemplate({ templateName: '[email protected]' });

expect(lifecycleMocks.preCommit).toHaveBeenCalledTimes(1);
expect(createInitialCommit).toHaveBeenCalledTimes(1);
expect(createInitialCommit).toHaveBeenNthCalledWith(1, './projectNameMock', { });
expect(lifecycleMocks.postCommit).toHaveBeenCalledTimes(1);
});

it('does not create initial commit if the lifecycle method indicates so', async () => {
lifecycleMocks.preCommit.mockReturnValueOnce({ skip: true });

await generateFromTemplate({ templateName: '[email protected]' });

expect(lifecycleMocks.preCommit).toHaveBeenCalledTimes(1);
expect(createInitialCommit).not.toHaveBeenCalled();
expect(lifecycleMocks.postCommit).not.toHaveBeenCalled();
});

it('does not create initial commit if git init was skipped', async () => {
lifecycleMocks.preGitInit.mockReturnValueOnce({ skip: true });

await generateFromTemplate({ templateName: '[email protected]' });

expect(lifecycleMocks.preCommit).not.toHaveBeenCalled();
expect(createInitialCommit).not.toHaveBeenCalled();
expect(lifecycleMocks.postCommit).not.toHaveBeenCalled();
});

it('should print the post generation message if it exists', async () => {
Expand Down
41 changes: 35 additions & 6 deletions src/generate-from-template.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ const getPackageName = require('./utils/get-package-name');
const getPackageVersion = require('./utils/get-package-version');
const { getStoredValues, setStoreValues } = require('./utils/storage');

const noop = () => ({});
const defaultLifecycleMethods = {
preGenerate: noop,
postGenerate: noop,
preGitInit: noop,
postGitInit: noop,
preInstall: noop,
postInstall: noop,
preCommit: noop,
postCommit: noop,
};

const generateFromTemplate = async ({ templateName }) => {
// Load the template
log.goToStep(1);
Expand Down Expand Up @@ -57,15 +69,17 @@ const generateFromTemplate = async ({ templateName }) => {
dynamicDirectoryNames = [],
ignoredFileNames = [],
ignoredDirectories = [],
lifecycle: configuredLifecycleMethods = {},
} = await templatePackage.getTemplateOptions(baseData, prompts, storedValues);
const lifecycle = { ...defaultLifecycleMethods, ...configuredLifecycleMethods };
if (generatorOptions.storeResponses) {
setStoreValues(templatePackageName, templateVersion, templateValues);
}
const templateDirPaths = templatePackage.getTemplatePaths();

// Generate Module
log.goToStep(3, templateBanner);

const { skip: skipGenerate } = { ...lifecycle.preGenerate() };
if (skipGenerate) console.warn('Cannot skip generation. Ignoring.');
templateDirPaths.forEach((templateRootPath) => walkTemplate(
templateRootPath,
`./${templateValues.projectName}`,
Expand All @@ -79,19 +93,34 @@ const generateFromTemplate = async ({ templateName }) => {
if (Object.keys(dynamicDirectoryNames).length > 0) {
renameDirectories(path.resolve(`./${templateValues.projectName}`), { dynamicDirectoryNames });
}
lifecycle.postGenerate();

// Initialize git before installing deps. This allows git hooks to be setup
// as part of install
log.goToStep(4, templateBanner);
await initializeGitRepo(`./${templateValues.projectName}`);
const { skip: skipGitInit } = { ...lifecycle.preGitInit() };
if (!skipGitInit) {
await initializeGitRepo(`./${templateValues.projectName}`);
lifecycle.postGitInit();
}

// Install and build the module
log.goToStep(5, templateBanner);
await installModule(`./${templateValues.projectName}`);
const { skip: skipInstall } = { ...lifecycle.preInstall() };
if (!skipInstall) {
await installModule(`./${templateValues.projectName}`);
lifecycle.postInstall();
}

// Create the first commit
log.goToStep(6, templateBanner);
await createInitialCommit(`./${templateValues.projectName}`, generatorOptions);
if (!skipGitInit) {
log.goToStep(6, templateBanner);
const { skip: skipCommit } = { ...lifecycle.preCommit() };
if (!skipCommit) {
await createInitialCommit(`./${templateValues.projectName}`, generatorOptions);
lifecycle.postCommit();
}
}

if (generatorOptions.postGenerationMessage) {
console.log(generatorOptions.postGenerationMessage);
Expand Down

0 comments on commit 741e33a

Please sign in to comment.