mirror of
https://github.com/strapi/strapi.git
synced 2025-08-08 08:46:42 +00:00
153 lines
5.0 KiB
JavaScript
153 lines
5.0 KiB
JavaScript
![]() |
'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 checkTemplateShape(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) {
|
||
|
// Import the package.json and template.json templates
|
||
|
const packageJSON = require(path.resolve(rootPath, 'package.json'));
|
||
|
const templateJSON = require(path.resolve(templatePath, 'template.json'));
|
||
|
|
||
|
// Use lodash to deeply merge them
|
||
|
const mergedConfig = _.merge(packageJSON, templateJSON);
|
||
|
// 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 checkTemplateShape(templatePath);
|
||
|
|
||
|
// Merge contents of the template in the project
|
||
|
await mergePackageJSON(rootPath, templatePath);
|
||
|
await mergeFilesAndDirectories(rootPath, templatePath);
|
||
|
|
||
|
// Delete the downloaded template repo
|
||
|
await fse.remove(templateParentPath);
|
||
|
};
|