From 24d42f385fa3f2edee1eaa914649a1aefb5740eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Fri, 4 Sep 2020 16:01:03 +0200 Subject: [PATCH] Apply review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémi de Juvigny --- docs/v3.x/concepts/templates.md | 51 +++++-- .../create-strapi-app/create-strapi-app.js | 4 +- .../strapi-generate-new/lib/create-project.js | 7 +- .../strapi-generate-new/lib/merge-template.js | 139 +++++++++++------- packages/strapi-generate-new/package.json | 3 +- yarn.lock | 72 ++++----- 6 files changed, 168 insertions(+), 108 deletions(-) diff --git a/docs/v3.x/concepts/templates.md b/docs/v3.x/concepts/templates.md index c2547bdb4a..6738fdb75d 100644 --- a/docs/v3.x/concepts/templates.md +++ b/docs/v3.x/concepts/templates.md @@ -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. diff --git a/packages/create-strapi-app/create-strapi-app.js b/packages/create-strapi-app/create-strapi-app.js index 6330067b20..174ad247d2 100644 --- a/packages/create-strapi-app/create-strapi-app.js +++ b/packages/create-strapi-app/create-strapi-app.js @@ -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); diff --git a/packages/strapi-generate-new/lib/create-project.js b/packages/strapi-generate-new/lib/create-project.js index 9130bf19fe..706e6b94e8 100644 --- a/packages/strapi-generate-new/lib/create-project.js +++ b/packages/strapi-generate-new/lib/create-project.js @@ -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); diff --git a/packages/strapi-generate-new/lib/merge-template.js b/packages/strapi-generate-new/lib/merge-template.js index c2d5644081..cb2031a9ab 100644 --- a/packages/strapi-generate-new/lib/merge-template.js +++ b/packages/strapi-generate-new/lib/merge-template.js @@ -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); }; diff --git a/packages/strapi-generate-new/package.json b/packages/strapi-generate-new/package.json index 85541d51a6..170be5ddc0 100644 --- a/packages/strapi-generate-new/package.json +++ b/packages/strapi-generate-new/package.json @@ -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": { diff --git a/yarn.lock b/yarn.lock index 5f6d30ead8..6b7425b84e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"