mirror of
https://github.com/strapi/strapi.git
synced 2025-07-24 09:25:25 +00:00

* Allow template configs as functions Signed-off-by: Rémi de Juvigny <remi@hey.com> * Allow template shorthands Signed-off-by: Rémi de Juvigny <remi@hey.com> * Restore strapi-generate-new import Signed-off-by: Rémi de Juvigny <remi@hey.com> * New shorthand system and updated docs Signed-off-by: Rémi de Juvigny <remi@hey.com> * Improved docs Signed-off-by: Rémi de Juvigny <remi@hey.com> * Remove duplicate merge-template file Signed-off-by: Rémi de Juvigny <remi@hey.com>
240 lines
7.9 KiB
JavaScript
240 lines
7.9 KiB
JavaScript
'use strict';
|
|
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const fse = require('fs-extra');
|
|
const fetch = require('node-fetch');
|
|
const tar = require('tar');
|
|
const _ = require('lodash');
|
|
const chalk = require('chalk');
|
|
const gitInfo = require('hosted-git-info');
|
|
|
|
// Specify all the files and directories a template can have
|
|
const allowChildren = '*';
|
|
const allowedTemplateContents = {
|
|
'README.md': true,
|
|
'.env.example': true,
|
|
api: allowChildren,
|
|
components: allowChildren,
|
|
config: {
|
|
functions: allowChildren,
|
|
},
|
|
data: allowChildren,
|
|
plugins: allowChildren,
|
|
public: allowChildren,
|
|
scripts: allowChildren,
|
|
};
|
|
|
|
/**
|
|
* merge template with new project being created
|
|
* @param {string} scope project creation params
|
|
* @param {string} rootPath project path
|
|
*/
|
|
module.exports = async function mergeTemplate(scope, rootPath) {
|
|
// Parse template info
|
|
const repoInfo = getRepoInfo(scope.template);
|
|
const { user, project } = repoInfo;
|
|
console.log(`Installing ${chalk.yellow(`${user}/${project}`)} template.`);
|
|
|
|
// Download template repository to a temporary directory
|
|
const templatePath = await fse.mkdtemp(path.join(os.tmpdir(), 'strapi-'));
|
|
|
|
try {
|
|
await downloadGithubRepo(repoInfo, templatePath);
|
|
} catch (error) {
|
|
throw Error(`Could not download ${chalk.yellow(`${user}/${project}`)} repository.`);
|
|
}
|
|
|
|
// Make sure the downloaded template matches the required format
|
|
const { templateConfig } = await checkTemplateRootStructure(templatePath, scope);
|
|
await checkTemplateContentsStructure(path.resolve(templatePath, 'template'));
|
|
|
|
// Merge contents of the template in the project
|
|
const fullTemplateUrl = `https://github.com/${user}/${project}`;
|
|
await mergePackageJSON(rootPath, templateConfig, fullTemplateUrl);
|
|
await mergeFilesAndDirectories(rootPath, templatePath);
|
|
|
|
// Delete the downloaded template repo
|
|
await fse.remove(templatePath);
|
|
};
|
|
|
|
// Make sure the template has the required top-level structure
|
|
async function checkTemplateRootStructure(templatePath, scope) {
|
|
// Make sure the root of the repo has a template.json or a template.js file
|
|
const templateJsonPath = path.join(templatePath, 'template.json');
|
|
const templateFunctionPath = path.join(templatePath, 'template.js');
|
|
|
|
// Store the template config, whether it comes from a JSON or a function
|
|
let templateConfig = {};
|
|
|
|
const hasJsonConfig = fse.existsSync(templateJsonPath);
|
|
if (hasJsonConfig) {
|
|
const jsonStat = await fse.stat(templateJsonPath);
|
|
if (!jsonStat.isFile()) {
|
|
throw new Error(`A template's ${chalk.green('template.json')} must be a file`);
|
|
}
|
|
templateConfig = require(templateJsonPath);
|
|
}
|
|
|
|
const hasFunctionConfig = fse.existsSync(templateFunctionPath);
|
|
if (hasFunctionConfig) {
|
|
const functionStat = await fse.stat(templateFunctionPath);
|
|
if (!functionStat.isFile()) {
|
|
throw new Error(`A template's ${chalk.green('template.js')} must be a file`);
|
|
}
|
|
// Get the config by passing the scope to the function
|
|
templateConfig = require(templateFunctionPath)(scope);
|
|
}
|
|
|
|
// Make sure there's exactly one template config file
|
|
if (!hasJsonConfig && !hasFunctionConfig) {
|
|
throw new Error(
|
|
`A template must have either a ${chalk.green('template.json')} or a ${chalk.green(
|
|
'template.js'
|
|
)} root file`
|
|
);
|
|
} else if (hasJsonConfig && hasFunctionConfig) {
|
|
throw new Error(
|
|
`A template cannot have both ${chalk.green('template.json')} and ${chalk.green(
|
|
'template.js'
|
|
)} root files`
|
|
);
|
|
}
|
|
|
|
// Make sure the root of the repo has a template folder
|
|
const templateDirPath = path.join(templatePath, 'template');
|
|
try {
|
|
const stat = await fse.stat(templateDirPath);
|
|
if (!stat.isDirectory()) {
|
|
throw Error(`A template must have a root ${chalk.green('template/')} directory`);
|
|
}
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
throw Error(`A template must have a root ${chalk.green('template/')} directory`);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
return { templateConfig };
|
|
}
|
|
|
|
// Traverse template tree to make sure each file and folder is allowed
|
|
async function checkTemplateContentsStructure(templateContentsPath) {
|
|
// Recursively check if each item in a directory is allowed
|
|
const checkPathContents = (pathToCheck, parents) => {
|
|
const contents = fse.readdirSync(pathToCheck);
|
|
contents.forEach(item => {
|
|
const nextParents = [...parents, item];
|
|
const matchingTreeValue = _.get(allowedTemplateContents, nextParents);
|
|
|
|
// Treat files and directories separately
|
|
const itemPath = path.resolve(pathToCheck, item);
|
|
const isDirectory = fse.statSync(itemPath).isDirectory();
|
|
|
|
if (matchingTreeValue === undefined) {
|
|
// Unknown paths are forbidden
|
|
throw Error(
|
|
`Illegal template structure, unknown path ${chalk.green(nextParents.join('/'))}`
|
|
);
|
|
}
|
|
|
|
if (matchingTreeValue === true) {
|
|
if (!isDirectory) {
|
|
// All good, the file is allowed
|
|
return;
|
|
}
|
|
throw Error(
|
|
`Illegal template structure, expected a file and got a directory at ${chalk.green(
|
|
nextParents.join('/')
|
|
)}`
|
|
);
|
|
}
|
|
|
|
if (isDirectory) {
|
|
if (matchingTreeValue === allowChildren) {
|
|
// All children are allowed
|
|
return;
|
|
}
|
|
// Check if the contents of the directory are allowed
|
|
checkPathContents(itemPath, nextParents);
|
|
} else {
|
|
throw Error(
|
|
`Illegal template structure, unknow file ${chalk.green(nextParents.join('/'))}`
|
|
);
|
|
}
|
|
});
|
|
};
|
|
|
|
checkPathContents(templateContentsPath, []);
|
|
}
|
|
|
|
function getRepoInfo(template) {
|
|
try {
|
|
const { user, project, default: urlStrategy } = gitInfo.fromUrl(template);
|
|
if (urlStrategy === 'https' || urlStrategy === 'http') {
|
|
// A full GitHub URL was provided, return username and project directly
|
|
return { user, project };
|
|
}
|
|
if (urlStrategy === 'shortcut') {
|
|
// A shorthand was provided, so prefix the project name with "strapi-template-"
|
|
return {
|
|
user,
|
|
project: `strapi-template-${project}`,
|
|
};
|
|
}
|
|
} catch (error) {
|
|
// If it's not a GitHub URL, then assume it's a shorthand for an official template
|
|
return {
|
|
user: 'strapi',
|
|
project: `strapi-template-${template}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
async function downloadGithubRepo(repoInfo, templatePath) {
|
|
// Download from GitHub
|
|
const { user, project } = repoInfo;
|
|
const codeload = `https://codeload.github.com/${user}/${project}/tar.gz/master`;
|
|
const response = await fetch(codeload);
|
|
if (!response.ok) {
|
|
throw Error(`Could not download the ${chalk.green(`${user}/${project}`)} repository`);
|
|
}
|
|
|
|
await new Promise(resolve => {
|
|
response.body.pipe(tar.extract({ strip: 1, cwd: templatePath })).on('close', resolve);
|
|
});
|
|
}
|
|
|
|
// Merge the template's template.json into the Strapi project's package.json
|
|
async function mergePackageJSON(rootPath, templateConfig, templateUrl) {
|
|
// Import the package.json as an object
|
|
const packageJSON = require(path.resolve(rootPath, 'package.json'));
|
|
|
|
if (!templateConfig.package) {
|
|
// Nothing to overwrite
|
|
return;
|
|
}
|
|
|
|
// Make sure the template.json doesn't overwrite the UUID
|
|
if (templateConfig.package.strapi && templateConfig.package.strapi.uuid) {
|
|
throw Error('A template cannot overwrite the Strapi UUID');
|
|
}
|
|
|
|
// Use lodash to deeply merge them
|
|
const mergedConfig = _.merge(packageJSON, templateConfig.package);
|
|
|
|
// Add starter info to package.json
|
|
_.set(mergedConfig, 'strapi.template', templateUrl);
|
|
|
|
// Save the merged config as the new package.json
|
|
const packageJSONPath = path.join(rootPath, 'package.json');
|
|
await fse.writeJSON(packageJSONPath, mergedConfig, { spaces: 2 });
|
|
}
|
|
|
|
// Merge all allowed files and directories
|
|
async function mergeFilesAndDirectories(rootPath, templatePath) {
|
|
const templateDir = path.join(templatePath, 'template');
|
|
await fse.copy(templateDir, rootPath, { overwrite: true, recursive: true });
|
|
}
|