Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add export-template command to cli #10331

Merged
merged 10 commits into from
May 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/strapi/bin/strapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ program
.description('Generate a basic plugin')
.action(getLocalScript('generate'));

// `$ strapi generate:template <directory>`
program
.command('generate:template <directory>')
.description('Generate template from Strapi project')
.action(getLocalScript('generate-template'));

program
.command('build')
.option('--clean', 'Remove the build and .cache folders', false)
Expand Down
120 changes: 120 additions & 0 deletions packages/strapi/lib/commands/__tests__/generate-template.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
'use strict';
jest.mock('fs-extra', () => ({
ensureDir: jest.fn(() => Promise.resolve()),
copy: jest.fn(() => Promise.resolve()),
pathExists: jest.fn(() => Promise.resolve()),
writeJSON: jest.fn(() => Promise.resolve()),
}));

const { resolve, join } = require('path');
const fse = require('fs-extra');
const inquirer = require('inquirer');

const exportTemplate = require('../generate-template');

describe('generate:template command', () => {
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.clearAllMocks();
});

it('creates a new template directory', async () => {
fse.pathExists.mockReturnValue(false);
const directory = '../test-dir';
const rootPath = resolve(directory);
const templatePath = join(rootPath, 'template');

await exportTemplate(directory);

expect(fse.pathExists).toHaveBeenCalledWith(templatePath);
expect(fse.ensureDir).toHaveBeenCalledWith(templatePath);
});

it.each(['api', 'components', 'config/functions/bootstrap.js', 'data'])(
'copies folder %s',
async item => {
// Mock the empty directory arg
fse.pathExists.mockReturnValueOnce(false);
// Mock the folder exists
fse.pathExists.mockReturnValue(true);
const directory = '../test-dir';
const rootPath = resolve(directory);
const templatePath = join(rootPath, 'template');

await exportTemplate(directory);

expect(fse.pathExists).toHaveBeenCalledWith(join(process.cwd(), item));
expect(fse.copy).toHaveBeenCalledWith(join(process.cwd(), item), join(templatePath, item));
}
);

it('creates a json config file', async () => {
fse.pathExists.mockReturnValue(false);
const directory = '../test-dir';
const rootPath = resolve(directory);

await exportTemplate(directory);

expect(fse.pathExists).toHaveBeenCalledWith(join(rootPath, 'template.json'));
expect(fse.writeJSON).toHaveBeenCalledWith(join(rootPath, 'template.json'), {});
});

describe('handles prompt input', () => {
it('replaces directory if confirmed', async () => {
fse.pathExists.mockReturnValue(true);
const mockInquiry = jest
.spyOn(inquirer, 'prompt')
.mockImplementationOnce(() => ({ confirm: true }));
const directory = '../test-dir';
const rootPath = resolve(directory);
const templatePath = join(rootPath, 'template');

await exportTemplate(directory);

expect(fse.pathExists).toHaveBeenCalledWith(templatePath);
expect(mockInquiry).toHaveBeenLastCalledWith(
expect.objectContaining({ message: expect.any(String), name: 'confirm', type: 'confirm' })
);
expect(fse.ensureDir).toHaveBeenCalled();
expect(fse.copy).toHaveBeenCalled();
});

it('does not replace existing config file', async () => {
fse.pathExists.mockReturnValue(true);
jest.spyOn(inquirer, 'prompt').mockImplementationOnce(() => ({ confirm: true }));
const directory = '../test-dir';
const rootPath = resolve(directory);

await exportTemplate(directory);
expect(fse.pathExists).toHaveBeenCalledWith(join(rootPath, 'template.json'));
expect(fse.writeJSON).not.toHaveBeenCalled();
});

it('exits if not confirmed', async () => {
fse.pathExists.mockReturnValue(true);
jest.spyOn(console, 'error').mockImplementation(() => {});
const mockInquiry = jest
.spyOn(inquirer, 'prompt')
.mockImplementationOnce(() => ({ confirm: false }));

const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('exit');
});
const directory = '../test-dir';
const rootPath = resolve(directory);
const templatePath = join(rootPath, 'template');

await exportTemplate(directory).catch(err => {
expect(err).toEqual(new Error('exit'));
});

expect(fse.pathExists).toHaveBeenCalledWith(templatePath);
expect(mockInquiry).toHaveBeenLastCalledWith(
expect.objectContaining({ message: expect.any(String), name: 'confirm', type: 'confirm' })
);
expect(mockExit).toHaveBeenCalledWith(0);
expect(fse.ensureDir).not.toHaveBeenCalled();
expect(fse.copy).not.toHaveBeenCalled();
});
});
});
97 changes: 97 additions & 0 deletions packages/strapi/lib/commands/generate-template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use strict';

const { resolve, join, basename } = require('path');
const fse = require('fs-extra');
const chalk = require('chalk');
const inquirer = require('inquirer');

// All directories that a template could need
const TEMPLATE_CONTENT = ['api', 'components', 'config/functions/bootstrap.js', 'data'];

/**
*
* @param {string} templatePath Absolute path to template directory
* @param {string} rootBase Name of the root directory
*/
async function copyContent(templatePath, rootBase) {
for (const item of TEMPLATE_CONTENT) {
try {
const pathToCopy = join(process.cwd(), item);

if (!(await fse.pathExists(pathToCopy))) {
continue;
}

await fse.copy(pathToCopy, join(templatePath, item));
const currentProjectBase = basename(process.cwd());
console.log(
`${chalk.green(
'success'
)}: copy ${currentProjectBase}/${item} => ${rootBase}/template/${item}`
);
} catch (error) {
console.error(`${chalk.red('error')}: ${error.message}`);
}
}
}

/**
*
* @param {string} rootPath Absolute path to the root directory
*/
async function writeTemplateJson(rootPath) {
try {
await fse.writeJSON(join(rootPath, 'template.json'), {});
console.log(`${chalk.green('success')}: create JSON config file`);
} catch (error) {
console.error(`${chalk.red('error')}: ${error.message}`);
}
}

/**
*
* @param {string} rootPath Absolute path to the root directory
* @returns boolean
*/
async function templateConfigExists(rootPath) {
const jsonConfig = await fse.pathExists(join(rootPath, 'template.json'));
const functionConfig = await fse.pathExists(join(rootPath, 'template.js'));

return jsonConfig || functionConfig;
}

module.exports = async function generateTemplate(directory) {
const rootPath = resolve(directory);

// Get path to template directory: <rootPath>/template
const templatePath = join(rootPath, 'template');

// Check if the template directory exists
const exists = await fse.pathExists(templatePath);
const rootBase = basename(rootPath);

if (exists) {
// Confirm the user wants to replace the existing template
const inquiry = await inquirer.prompt({
type: 'confirm',
name: 'confirm',
message: `${chalk.yellow(rootBase)} already exists. Do you want to replace it?`,
});

if (!inquiry.confirm) {
process.exit(0);
}
}

// Create or replace root directory with <roothPath>/template
await fse.ensureDir(templatePath);
// Copy content to /template
await copyContent(templatePath, rootBase);
// Create config file if it doesn't exist
const configExists = await templateConfigExists(rootPath);
if (!configExists) {
await writeTemplateJson(rootPath);
}

console.log(`${chalk.green('success')}: generated template at ${chalk.yellow(rootPath)}`);
};