165 lines
5.4 KiB
JavaScript
Raw Normal View History

'use strict';
const path = require('path');
const fse = require('fs-extra');
const fetch = require('node-fetch');
const unzip = require('unzip-stream');
const _ = require('lodash');
// Specify all the files and directories a template can have
const allowChildren = '*';
const allowedTemplateTree = {
// Root template files
'template.json': true,
'README.md': true,
'.gitignore': true,
// Template contents
template: {
'README.md': true,
'.env.example': true,
api: allowChildren,
components: allowChildren,
config: {
functions: allowChildren,
},
data: allowChildren,
plugins: allowChildren,
public: allowChildren,
scripts: allowChildren,
},
};
// Traverse template tree to make sure each file and folder is allowed
async function checkTemplateStructure(templatePath) {
// Recursively check if each item in the template is allowed
const checkPathContents = (pathToCheck, parents) => {
const contents = fse.readdirSync(pathToCheck);
contents.forEach(item => {
const nextParents = [...parents, item];
const matchingTreeValue = _.get(allowedTemplateTree, 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 ${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 ${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 ${nextParents.join('/')}`);
}
});
};
checkPathContents(templatePath, []);
}
function getRepoInfo(githubURL) {
// Make sure it's a github url
const address = githubURL.split('://')[1];
if (!address.startsWith('github.com')) {
throw Error('A Strapi template can only be a GitHub URL');
}
// Parse github address into parts
const [, username, name, ...rest] = address.split('/');
const isRepo = username != null && name != null;
if (!isRepo || rest.length > 0) {
throw Error('A template URL must be the root of a GitHub repository');
}
return { username, name };
}
async function downloadGithubRepo(repoInfo, rootPath) {
const { username, name, branch = 'master' } = repoInfo;
const codeload = `https://codeload.github.com/${username}/${name}/zip/${branch}`;
const templatePath = path.resolve(rootPath, 'tmp-template');
const response = await fetch(codeload);
await new Promise(resolve => {
response.body.pipe(unzip.Extract({ path: templatePath })).on('close', resolve);
});
return templatePath;
}
// Merge the template's template.json into the Strapi project's package.json
async function mergePackageJSON(rootPath, templatePath, repoInfo) {
// Import the package.json and template.json templates
const packageJSON = require(path.resolve(rootPath, 'package.json'));
const templateJSON = require(path.resolve(templatePath, 'template.json'));
// Make sure the template.json doesn't overwrite the UUID
if (templateJSON.strapi && templateJSON.strapi.uuid) {
throw Error('A template cannot overwrite the Strapi UUID');
}
// Use lodash to deeply merge them
const mergedConfig = _.merge(packageJSON, templateJSON);
// Prefix Strapi UUID with starter info
const prefix = `STARTER:${repoInfo.username}/${repoInfo.name}:`;
mergedConfig.strapi = {
uuid: prefix + mergedConfig.strapi.uuid,
};
// Save the merged config as the new package.json
await fse.writeJSON(path.resolve(rootPath, 'package.json'), mergedConfig, {
spaces: 2,
});
}
// Merge all allowed files and directories
async function mergeFilesAndDirectories(rootPath, templatePath) {
const copyPromises = fse
.readdirSync(path.resolve(templatePath, 'template'))
.map(async fileOrDir => {
// Copy the template contents from the downloaded template to the Strapi project
await fse.copy(
path.resolve(templatePath, 'template', fileOrDir),
path.resolve(rootPath, fileOrDir),
{ overwrite: true, recursive: true }
);
});
await Promise.all(copyPromises);
}
module.exports = async function mergeTemplate(templateUrl, rootPath) {
// Download template repository to a temporary directory
const repoInfo = getRepoInfo(templateUrl);
console.log(`Installing ${repoInfo.username}/${repoInfo.name} template.`);
const templateParentPath = await downloadGithubRepo(repoInfo, rootPath);
const templatePath = path.resolve(templateParentPath, fse.readdirSync(templateParentPath)[0]);
// Make sure the downloaded template matches the required format
await checkTemplateStructure(templatePath);
// Merge contents of the template in the project
await mergePackageJSON(rootPath, templatePath, repoInfo);
await mergeFilesAndDirectories(rootPath, templatePath);
// Delete the downloaded template repo
await fse.remove(templateParentPath);
};