diff --git a/docs/commands/sites.md b/docs/commands/sites.md index aa1d956bfc9..40f6c267403 100644 --- a/docs/commands/sites.md +++ b/docs/commands/sites.md @@ -71,6 +71,10 @@ Create a site from a starter template. netlify sites:create-template ``` +**Arguments** + +- repository - repository to use as starter template + **Flags** - `account-slug` (*string*) - account slug to create the site under @@ -81,6 +85,14 @@ netlify sites:create-template - `httpProxy` (*string*) - Proxy server address to route requests through. - `httpProxyCertificateFilename` (*string*) - Certificate file to use when connecting using a proxy server +**Examples** + +```bash +netlify sites:create-template +netlify sites:create-template nextjs-blog-theme +netlify sites:create-template my-github-profile/my-template +``` + --- ## `sites:delete` diff --git a/src/commands/sites/sites-create-template.js b/src/commands/sites/sites-create-template.js index b9dfe26a665..a9be596a0b7 100644 --- a/src/commands/sites/sites-create-template.js +++ b/src/commands/sites/sites-create-template.js @@ -2,12 +2,14 @@ const inquirer = require('inquirer') const pick = require('lodash/pick') +const parseGitHubUrl = require('parse-github-url') const prettyjson = require('prettyjson') +const terminalLink = require('terminal-link') const { chalk, error, getRepoData, log, logJson, track, warn } = require('../../utils') const { configureRepo } = require('../../utils/init/config') const { getGitHubToken } = require('../../utils/init/config-github') -const { createRepo, getTemplatesFromGitHub } = require('../../utils/sites/utils') +const { createRepo, getTemplatesFromGitHub, validateTemplate } = require('../../utils/sites/utils') const { getSiteNameInput } = require('./sites-create') @@ -23,12 +25,45 @@ const fetchTemplates = async (token) => { })) } +const getTemplateName = async ({ ghToken, options, repository }) => { + if (repository) { + const { repo } = parseGitHubUrl(repository) + return repo || `netlify-templates/${repository}` + } + + if (options.url) { + const urlFromOptions = new URL(options.url) + return urlFromOptions.pathname.slice(1) + } + + const templates = await fetchTemplates(ghToken) + + log(`Choose one of our starter templates. Netlify will create a new repo for this template in your GitHub account.`) + + const { templateName } = await inquirer.prompt([ + { + type: 'list', + name: 'templateName', + message: 'Template:', + choices: templates.map((template) => ({ + value: template.slug, + name: template.name, + })), + }, + ]) + + return templateName +} + +const getGitHubLink = ({ options, templateName }) => options.url || `https://github.com/${templateName}` + /** * The sites:create-template command + * @param repository {string} * @param {import('commander').OptionValues} options * @param {import('../base-command').BaseCommand} command */ -const sitesCreateTemplate = async (options, command) => { +const sitesCreateTemplate = async (repository, options, command) => { const { api } = command.netlify await command.authenticate() @@ -36,27 +71,22 @@ const sitesCreateTemplate = async (options, command) => { const { globalConfig } = command.netlify const ghToken = await getGitHubToken({ globalConfig }) - let { url: templateUrl } = options - - if (templateUrl) { - const urlFromOptions = new URL(templateUrl) - templateUrl = { templateName: urlFromOptions.pathname.slice(1) } - } else { - const templates = await fetchTemplates(ghToken) - - log(`Choose one of our starter templates. Netlify will create a new repo for this template in your GitHub account.`) - - templateUrl = await inquirer.prompt([ - { - type: 'list', - name: 'templateName', - message: 'Template:', - choices: templates.map((template) => ({ - value: template.slug, - name: template.name, - })), - }, - ]) + const templateName = await getTemplateName({ ghToken, options, repository }) + const { exists, isTemplate } = await validateTemplate({ templateName, ghToken }) + if (!exists) { + const githubLink = getGitHubLink({ options, templateName }) + error( + `Could not find template ${chalk.bold(templateName)}. Please verify it exists and you can ${terminalLink( + 'access to it on GitHub', + githubLink, + )}`, + ) + return + } + if (!isTemplate) { + const githubLink = getGitHubLink({ options, templateName }) + error(`${terminalLink(chalk.bold(templateName), githubLink)} is not a valid GitHub template`) + return } const accounts = await api.listAccountsForUser() @@ -90,12 +120,12 @@ const sitesCreateTemplate = async (options, command) => { const siteName = inputName ? inputName.trim() : siteSuggestion // Create new repo from template - const repoResp = await createRepo(templateUrl, ghToken, siteName) + const repoResp = await createRepo(templateName, ghToken, siteName) if (repoResp.errors) { if (repoResp.errors[0].includes('Name already exists on this account')) { warn( - `Oh no! We found already a repository with this name. It seems you have already created a template with the name ${templateUrl.templateName}. Please try to run the command again and provide a different name.`, + `Oh no! We found already a repository with this name. It seems you have already created a template with the name ${templateName}. Please try to run the command again and provide a different name.`, ) await inputSiteName() } else { @@ -206,7 +236,13 @@ Create a site from a starter template.`, .option('-u, --url [url]', 'template url') .option('-a, --account-slug [slug]', 'account slug to create the site under') .option('-c, --with-ci', 'initialize CI hooks during site creation') + .argument('[repository]', 'repository to use as starter template') .addHelpText('after', `(Beta) Create a site from starter template.`) + .addExamples([ + 'netlify sites:create-template', + 'netlify sites:create-template nextjs-blog-theme', + 'netlify sites:create-template my-github-profile/my-template', + ]) .action(sitesCreateTemplate) module.exports = { createSitesFromTemplateCommand, fetchTemplates } diff --git a/src/utils/sites/utils.js b/src/utils/sites/utils.js index 66d74f03896..9b1aec85cd1 100644 --- a/src/utils/sites/utils.js +++ b/src/utils/sites/utils.js @@ -12,8 +12,27 @@ const getTemplatesFromGitHub = async (token) => { return allTemplates } -const createRepo = async (templateUrl, ghToken, siteName) => { - const resp = await fetch(`https://api.github.com/repos/${templateUrl.templateName}/generate`, { +const validateTemplate = async ({ ghToken, templateName }) => { + const response = await fetch(`https://api.github.com/repos/${templateName}`, { + headers: { + Authorization: `token ${ghToken}`, + }, + }) + + if (response.status === 404) { + return { exists: false } + } + + if (!response.ok) { + throw new Error(`Error fetching template ${templateName}: ${await response.text()}`) + } + + const data = await response.json() + return { exists: true, isTemplate: data.is_template } +} + +const createRepo = async (templateName, ghToken, siteName) => { + const resp = await fetch(`https://api.github.com/repos/${templateName}/generate`, { method: 'POST', headers: { Authorization: `token ${ghToken}`, @@ -27,4 +46,4 @@ const createRepo = async (templateUrl, ghToken, siteName) => { return data } -module.exports = { getTemplatesFromGitHub, createRepo } +module.exports = { getTemplatesFromGitHub, createRepo, validateTemplate } diff --git a/tests/integration/140.command.sites.test.js b/tests/integration/140.command.sites.test.js index 6189077cd45..da481c1e31d 100644 --- a/tests/integration/140.command.sites.test.js +++ b/tests/integration/140.command.sites.test.js @@ -30,6 +30,11 @@ const createRepoStub = sinon.stub(templatesUtils, 'createRepo').callsFake(() => branch: 'main', })) +const validateTemplateStub = sinon.stub(templatesUtils, 'validateTemplate').callsFake(() => ({ + exists: true, + isTemplate: true, +})) + const jsonRenderSpy = sinon.spy(prettyjson, 'render') const { createSitesFromTemplateCommand, fetchTemplates } = require('../../src/commands/sites/sites-create-template') @@ -37,11 +42,15 @@ const { createSitesFromTemplateCommand, fetchTemplates } = require('../../src/co /* eslint-enable import/order */ const { withMockApi } = require('./utils/mock-api') +const inquirerStub = sinon.stub(inquirer, 'prompt').callsFake(() => Promise.resolve({ accountSlug: 'test-account' })) + test.beforeEach(() => { + inquirerStub.resetHistory() gitMock.resetHistory() getTemplatesStub.resetHistory() createRepoStub.resetHistory() jsonRenderSpy.resetHistory() + validateTemplateStub.resetHistory() }) const siteInfo = { @@ -74,8 +83,6 @@ const routes = [ ] test.serial('netlify sites:create-template', async (t) => { - const inquirerStub = sinon.stub(inquirer, 'prompt').callsFake(() => Promise.resolve({ accountSlug: 'test-account' })) - await withMockApi(routes, async ({ apiUrl }) => { Object.defineProperty(process, 'env', { value: { @@ -103,12 +110,9 @@ test.serial('netlify sites:create-template', async (t) => { }), ) }) - inquirerStub.restore() }) test.serial('should not fetch templates if one is passed as option', async (t) => { - const inquirerStub = sinon.stub(inquirer, 'prompt').callsFake(() => Promise.resolve({ accountSlug: 'test-account' })) - await withMockApi(routes, async ({ apiUrl }) => { Object.defineProperty(process, 'env', { value: { @@ -131,12 +135,9 @@ test.serial('should not fetch templates if one is passed as option', async (t) = t.truthy(getTemplatesStub.notCalled) }) - inquirerStub.restore() }) test.serial('should throw an error if the URL option is not a valid URL', async (t) => { - const inquirerStub = sinon.stub(inquirer, 'prompt').callsFake(() => Promise.resolve({ accountSlug: 'test-account' })) - await withMockApi(routes, async ({ apiUrl }) => { Object.defineProperty(process, 'env', { value: { @@ -155,7 +156,6 @@ test.serial('should throw an error if the URL option is not a valid URL', async t.truthy(error.message.includes('Invalid URL')) }) - inquirerStub.restore() }) test.serial('should return an array of templates with name, source code url and slug', async (t) => {