Apply review feedback

Signed-off-by: Rémi de Juvigny <remi@hey.com>
This commit is contained in:
Rémi de Juvigny 2020-09-04 16:01:03 +02:00
parent e327d8ccfd
commit 24d42f385f
6 changed files with 168 additions and 108 deletions

View File

@ -48,23 +48,42 @@ To create a Strapi template, you need to publish a public GitHub repository that
First, a template's only concern should be to adapt Strapi to a use case. It should not deal with environment-specific configs, like databases, or upload and email providers. This is to make sure that templates stay maintainable, and to avoid conflicts with other CLI options like `--quickstart`.
Second, a template must follow the following file structure. If any unexpected file or directory is found, the installation will crash.
Second, a template must follow the following file structure.
### File structure
- `README.md`: to document your template
- `.gitignore`: to remove files from Git
- `template.json`: to extend the Strapi app's default `package.json`
- `/template`: where you can extend the file contents of a Strapi project. All the children are optional
- `README.md`: the readme of an app made with this template
- `.env.example`: to specify required environment variables
- `api/`: for collections and single types
- `components/` for components
- `config/` can only include the `functions` directory (things like `bootstrap.js` or `404.js`), because other config files are environment-specific.
- `data/` to store the data imported by a seed script
- `plugins/` for custom Strapi plugins
- `public/` to serve files
- `scripts/` for custom scripts
You can add as many files as you want to the root of your template repository. But it must at least have a `template.json` file and a `template` directory.
The `template.json` is used to extend the Strapi app's default `package.json`. You can put all the properties that should overwrite the default `package.json` in a root `package` property. For example, a `template.json` might look like this:
```json
{
"package": {
"dependencies": {
"strapi-plugin-graphql": "latest"
},
"scripts": {
"custom": "node ./scripts/custom.js"
}
}
}
```
The `template` directory is where you can extend the file contents of a Strapi project. All the children are optional, you should only include the files that will overwrite the default Strapi app.
Only the following contents are allowed inside the `template` directory:
- `README.md`: the readme of an app made with this template
- `.env.example`: to specify required environment variables
- `api/`: for collections and single types
- `components/` for components
- `config/` can only include the `functions` directory (things like `bootstrap.js` or `404.js`), because other config files are environment-specific.
- `data/` to store the data imported by a seed script
- `plugins/` for custom Strapi plugins
- `public/` to serve files
- `scripts/` for custom scripts
If any unexpected file or directory is found, the installation will crash.
### Step by step
@ -73,8 +92,8 @@ After reading the above rules, follow these steps to create your template:
1. Create a standard Strapi app with `create-strapi-app`, using the `--quickstart` option.
2. Customize your app to match the needs of your use case.
3. Outside of Strapi, create a new directory for your template.
4. Create `template.json`, `.gitignore` and `README.md` files in your template directory.
5. If you have modified your app's `package.json`, include these changes (and _only_ these changes) in `template.json`. Otherwise, leave it as an empty object.
4. Create a `template.json` file in your template directory.
5. If you have modified your app's `package.json`, include these changes (and _only_ these changes) in `template.json` in a `package` property. Otherwise, leave it as an empty object.
6. Create a `/template` subdirectory.
7. Think of all the files you have modified in your app, and copy them to the `/template` directory
8. Publish the root template project on GitHub. Make sure that the repository is public, and that the code is on the `master` branch.

View File

@ -2,9 +2,7 @@
const commander = require('commander');
const packageJson = require('./package.json');
// TODO: restore this line instead:
// const generateNewApp = require('strapi-generate-new');
const generateNewApp = require('../strapi-generate-new');
const generateNewApp = require('strapi-generate-new');
const program = new commander.Command(packageJson.name);

View File

@ -70,7 +70,12 @@ module.exports = async function createProject(scope, { client, connection, depen
// merge template files if a template is specified
const hasTemplate = Boolean(scope.template);
if (hasTemplate) {
await mergeTemplate(scope.template, rootPath);
try {
await mergeTemplate(scope.template, rootPath);
} catch (error) {
await fse.remove(scope.rootPath);
stopProcess(`⛔️ Template installation failed: ${error.message}`);
}
}
} catch (err) {
await fse.remove(scope.rootPath);

View File

@ -3,40 +3,60 @@
const path = require('path');
const fse = require('fs-extra');
const fetch = require('node-fetch');
const unzip = require('unzip-stream');
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 allowedTemplateTree = {
// Root template files
'template.json': true,
const allowedTemplateContents = {
'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,
'.env.example': true,
api: allowChildren,
components: allowChildren,
config: {
functions: allowChildren,
},
data: allowChildren,
plugins: allowChildren,
public: allowChildren,
scripts: allowChildren,
};
// Make sure the template has the required top-level structure
async function checkTemplateRootStructure(templatePath) {
// Make sure the root of the repo has a template.json and a template/ folder
const templateJsonPath = path.resolve(templatePath, 'template.json');
try {
const hasTemplateJson = !fse.statSync(templateJsonPath).isDirectory();
if (!hasTemplateJson) {
throw Error();
}
} catch (error) {
throw Error(`A template must have a root ${chalk.green('template.json')} file`);
}
// Make sure the root of the repo has a template.json and a template/ folder
const templateDirPath = path.resolve(templatePath, 'template');
try {
const hasTemplateDir = fse.statSync(templateDirPath).isDirectory();
if (!hasTemplateDir) {
throw Error();
}
} catch (error) {
throw Error(`A template must have a root ${chalk.green('template/')} directory`);
}
}
// 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
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(allowedTemplateTree, nextParents);
const matchingTreeValue = _.get(allowedTemplateContents, nextParents);
// Treat files and directories separately
const itemPath = path.resolve(pathToCheck, item);
@ -44,7 +64,9 @@ async function checkTemplateStructure(templatePath) {
if (matchingTreeValue === undefined) {
// Unknown paths are forbidden
throw Error(`Illegal template structure, unknown path ${nextParents.join('/')}`);
throw Error(
`Illegal template structure, unknown path ${chalk.green(nextParents.join('/'))}`
);
}
if (matchingTreeValue === true) {
@ -53,8 +75,8 @@ async function checkTemplateStructure(templatePath) {
return;
}
throw Error(
`Illegal template structure, expected a file and got a directory at ${nextParents.join(
'/'
`Illegal template structure, expected a file and got a directory at ${chalk.green(
nextParents.join('/')
)}`
);
}
@ -67,59 +89,60 @@ async function checkTemplateStructure(templatePath) {
// Check if the contents of the directory are allowed
checkPathContents(itemPath, nextParents);
} else {
throw Error(`Illegal template structure, unknow file ${nextParents.join('/')}`);
throw Error(
`Illegal template structure, unknow file ${chalk.green(nextParents.join('/'))}`
);
}
});
};
checkPathContents(templatePath, []);
checkPathContents(templateContentsPath, []);
}
function getRepoInfo(githubURL) {
const { type, user, project } = gitInfo.fromUrl(githubURL);
// Make sure it's a github url
const address = githubURL.split('://')[1];
if (!address.startsWith('github.com')) {
if (type !== 'github') {
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 };
return { user, project };
}
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');
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);
await new Promise(resolve => {
response.body.pipe(unzip.Extract({ path: templatePath })).on('close', resolve);
});
return templatePath;
// Store locally
fse.mkdirSync(templatePath);
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, templatePath, repoInfo) {
// Import the package.json and template.json templates
// Import the package.json and template.json objects
const packageJSON = require(path.resolve(rootPath, 'package.json'));
const templateJSON = require(path.resolve(templatePath, 'template.json'));
if (!templateJSON.package) {
// Nothing to overwrite
return;
}
// Make sure the template.json doesn't overwrite the UUID
if (templateJSON.strapi && templateJSON.strapi.uuid) {
if (templateJSON.package.strapi && templateJSON.package.strapi.uuid) {
throw Error('A template cannot overwrite the Strapi UUID');
}
// Use lodash to deeply merge them
const mergedConfig = _.merge(packageJSON, templateJSON);
const mergedConfig = _.merge(packageJSON, templateJSON.package);
// Prefix Strapi UUID with starter info
const prefix = `STARTER:${repoInfo.username}/${repoInfo.name}:`;
const prefix = `STARTER:${repoInfo.user}/${repoInfo.project}:`;
mergedConfig.strapi = {
uuid: prefix + mergedConfig.strapi.uuid,
};
@ -146,19 +169,27 @@ async function mergeFilesAndDirectories(rootPath, templatePath) {
}
module.exports = async function mergeTemplate(templateUrl, rootPath) {
// Download template repository to a temporary directory
// Parse template info
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]);
const { user, project } = repoInfo;
console.log(`Installing ${chalk.yellow(`${user}/${project}`)} template.`);
// Download template repository to a temporary directory
const templatePath = path.resolve(rootPath, '.tmp-template');
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
await checkTemplateStructure(templatePath);
await checkTemplateRootStructure(templatePath);
await checkTemplateContentsStructure(path.resolve(templatePath, 'template'));
// 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);
await fse.remove(templatePath);
};

View File

@ -17,12 +17,13 @@
"chalk": "^2.4.2",
"execa": "^1.0.0",
"fs-extra": "^8.0.1",
"hosted-git-info": "3.0.5",
"inquirer": "^6.3.1",
"lodash": "4.17.19",
"node-fetch": "^1.7.3",
"node-machine-id": "^1.1.10",
"ora": "^3.4.0",
"unzip-stream": "0.3.0",
"tar": "6.0.5",
"uuid": "^3.3.2"
},
"scripts": {

View File

@ -4622,14 +4622,6 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
binary@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79"
integrity sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=
dependencies:
buffers "~0.1.1"
chainsaw "~0.1.0"
bindings@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
@ -4939,11 +4931,6 @@ buffer@^5.1.0:
base64-js "^1.0.2"
ieee754 "^1.1.4"
buffers@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb"
integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s=
buildmail@3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/buildmail/-/buildmail-3.10.0.tgz#c6826d716e7945bb6f6b1434b53985e029a03159"
@ -5178,13 +5165,6 @@ ccount@^1.0.0:
resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17"
integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==
chainsaw@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98"
integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=
dependencies:
traverse ">=0.3.0 <0.4"
chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@ -5302,6 +5282,11 @@ chownr@^1.1.1, chownr@^1.1.2, chownr@^1.1.3:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
chrome-trace-event@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
@ -9293,6 +9278,13 @@ homedir-polyfill@^1.0.1:
dependencies:
parse-passwd "^1.0.0"
hosted-git-info@3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.5.tgz#bea87905ef7317442e8df3087faa3c842397df03"
integrity sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ==
dependencies:
lru-cache "^6.0.0"
hosted-git-info@^2.1.4, hosted-git-info@^2.7.1:
version "2.8.8"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
@ -12116,6 +12108,13 @@ lru-cache@^5.0.0, lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
lru_map@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd"
@ -12643,6 +12642,14 @@ minizlib@^2.1.0:
minipass "^3.0.0"
yallist "^4.0.0"
minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
dependencies:
minipass "^3.0.0"
yallist "^4.0.0"
mississippi@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
@ -18134,6 +18141,18 @@ tar-stream@^2.0.0, tar-stream@^2.1.0:
inherits "^2.0.3"
readable-stream "^3.1.1"
tar@6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f"
integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^3.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
tar@^4, tar@^4.4.10, tar@^4.4.12, tar@^4.4.8:
version "4.4.13"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
@ -18494,11 +18513,6 @@ tr46@^2.0.2:
dependencies:
punycode "^2.1.1"
"traverse@>=0.3.0 <0.4":
version "0.3.9"
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=
tree-kill@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
@ -18849,14 +18863,6 @@ unzip-response@^2.0.1:
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=
unzip-stream@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/unzip-stream/-/unzip-stream-0.3.0.tgz#c30c054cd6b0d64b13a23cd3ece911eb0b2b52d8"
integrity sha512-NG1h/MdGIX3HzyqMjyj1laBCmlPYhcO4xEy7gEqqzGiSLw7XqDQCnY4nYSn5XSaH8mQ6TFkaujrO8d/PIZN85A==
dependencies:
binary "^0.3.0"
mkdirp "^0.5.1"
upath@^1.1.1, upath@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"