mirror of
https://github.com/strapi/strapi.git
synced 2025-07-31 12:55:08 +00:00
233 lines
8.3 KiB
JavaScript
233 lines
8.3 KiB
JavaScript
'use strict';
|
|
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const fse = require('fs-extra');
|
|
const _ = require('lodash/fp');
|
|
const chalk = require('chalk');
|
|
const semver = require('semver');
|
|
const { getTemplatePackageInfo, downloadNpmTemplate } = require('./fetch-npm-template');
|
|
|
|
// Specify all the files and directories a template can have
|
|
const allowFile = Symbol();
|
|
const allowChildren = Symbol();
|
|
const allowedTemplateContents = {
|
|
'README.md': allowFile,
|
|
'.env.example': allowFile,
|
|
'package.json': allowFile,
|
|
src: allowChildren,
|
|
data: allowChildren,
|
|
database: 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) {
|
|
let templatePath;
|
|
let templateParentPath;
|
|
let templatePackageInfo = {};
|
|
const isLocalTemplate = ['./', '../'].some(filePrefix => scope.template.startsWith(filePrefix));
|
|
|
|
if (isLocalTemplate) {
|
|
// Template is a local directory
|
|
console.log('Installing local template.');
|
|
templatePath = path.resolve(rootPath, '..', scope.template);
|
|
} else {
|
|
// Template should be an npm package. Fetch template info
|
|
templatePackageInfo = await getTemplatePackageInfo(scope.template);
|
|
console.log(`Installing ${chalk.yellow(templatePackageInfo.name)} template.`);
|
|
|
|
// Download template repository to a temporary directory
|
|
templateParentPath = await fse.mkdtemp(path.join(os.tmpdir(), 'strapi-'));
|
|
templatePath = downloadNpmTemplate(templatePackageInfo, templateParentPath);
|
|
}
|
|
|
|
// Make sure the template is compatible with this version of strapi
|
|
checkTemplateCompat(templatePath, scope.strapiVersion);
|
|
|
|
// 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
|
|
await mergePackageJSON({ rootPath, templateConfig, templatePackageInfo });
|
|
await mergeFilesAndDirectories(rootPath, templatePath);
|
|
|
|
// Delete the template directory if it was downloaded
|
|
if (!isLocalTemplate) {
|
|
await fse.remove(templateParentPath);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Make sure the template is compatible with a specific Strapi version
|
|
* @param {string} templatePath - Where the template is installed
|
|
* @param {string} strapiVersion - Strapi version of the app being created
|
|
*/
|
|
function checkTemplateCompat(templatePath, strapiVersion) {
|
|
const templateJSON = require(path.resolve(templatePath, 'template.json'));
|
|
const compatibleStrapiConstraint = _.get('strapi.supportedVersion', templateJSON);
|
|
|
|
// Make sure the Strapi compatibility constraint is set
|
|
if (_.isNil(compatibleStrapiConstraint)) {
|
|
throw new Error("This template does not specify the Strapi versions it's compatible with");
|
|
}
|
|
|
|
// Check that the range is set using proper semver
|
|
const validCompatibleStrapiRange = semver.validRange(compatibleStrapiConstraint);
|
|
if (!validCompatibleStrapiRange) {
|
|
throw new Error(
|
|
'Please use a valid semver constraint to specify which Strapi versions this template is compatible with'
|
|
);
|
|
}
|
|
|
|
// Check if the template is compatible with this Strapi version
|
|
const isCompatible = semver.satisfies(strapiVersion, validCompatibleStrapiRange);
|
|
if (!isCompatible) {
|
|
throw new Error(`
|
|
This template is not compatible with Strapi version ${strapiVersion}.
|
|
It will only work with Strapi versions in the constraint ${chalk.green(
|
|
JSON.stringify(compatibleStrapiConstraint)
|
|
)}.
|
|
Try using a different Strapi version, or a different version of this template instead.
|
|
`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make sure the template has the required top-level structure
|
|
* @param {string} templatePath - Path of the locally downloaded template
|
|
* @returns {Object} - The template config object
|
|
*/
|
|
async function checkTemplateRootStructure(templatePath) {
|
|
// Make sure the root of the repo has a template.json file
|
|
const templateJsonPath = path.join(templatePath, 'template.json');
|
|
const templateJsonExists = await fse.exists(templateJsonPath);
|
|
if (!templateJsonExists) {
|
|
throw new Error(`A template must have a ${chalk.green('template.json')} root file`);
|
|
}
|
|
const templateJsonStat = await fse.stat(templateJsonPath);
|
|
if (!templateJsonStat.isFile()) {
|
|
throw new Error(`A template's ${chalk.green('template.json')} must be a file`);
|
|
}
|
|
|
|
const templateConfig = require(templateJsonPath);
|
|
|
|
// 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
|
|
* @param {string} templateContentsPath
|
|
*/
|
|
async function checkTemplateContentsStructure(templateContentsPath) {
|
|
// Recursively check if each item in a directory is allowed
|
|
const checkPathContents = async (pathToCheck, parents) => {
|
|
const contents = await fse.readdir(pathToCheck);
|
|
for (const item of contents) {
|
|
const nextParents = [...parents, item];
|
|
const matchingTreeValue = _.get(nextParents, allowedTemplateContents);
|
|
|
|
// Treat files and directories separately
|
|
const itemPath = path.resolve(pathToCheck, item);
|
|
const isDirectory = (await fse.stat(itemPath)).isDirectory();
|
|
|
|
if (matchingTreeValue === undefined) {
|
|
// Unknown paths are forbidden
|
|
throw Error(
|
|
`Illegal template structure, unknown path ${chalk.green(nextParents.join('/'))}`
|
|
);
|
|
}
|
|
|
|
if (matchingTreeValue === allowFile) {
|
|
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
|
|
await checkPathContents(itemPath, nextParents);
|
|
} else {
|
|
throw Error(
|
|
`Illegal template structure, unknow file ${chalk.green(nextParents.join('/'))}`
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
await checkPathContents(templateContentsPath, []);
|
|
}
|
|
|
|
/**
|
|
* Merge the template's template.json into the Strapi project's package.json
|
|
* @param {Object} config
|
|
* @param {string} config.rootPath
|
|
* @param {string} config.templateConfig
|
|
* @param {Object} config.templatePackageInfo - Info about the template's package on npm
|
|
* @param {Object} config.templatePackageInfo.name - The name of the template's package on npm
|
|
* @param {Object} config.templatePackageInfo.version - The name of the template's package on npm
|
|
*/
|
|
async function mergePackageJSON({ rootPath, templateConfig, templatePackageInfo }) {
|
|
// 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(templateConfig.package, packageJSON);
|
|
|
|
// Add template info to package.json
|
|
if (templatePackageInfo.name) {
|
|
_.set('strapi.template', templatePackageInfo.name, mergedConfig);
|
|
}
|
|
|
|
// 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 });
|
|
}
|