Skip to content

Commit

Permalink
feat(sites-create-template): add repo shorthand support (#4382)
Browse files Browse the repository at this point in the history
  • Loading branch information
iib0011 authored Mar 2, 2022
1 parent 928df56 commit 4e74045
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 37 deletions.
12 changes: 12 additions & 0 deletions docs/commands/sites.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`

Expand Down
86 changes: 61 additions & 25 deletions src/commands/sites/sites-create-template.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -23,40 +25,68 @@ 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()

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()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 }
25 changes: 22 additions & 3 deletions src/utils/sites/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand All @@ -27,4 +46,4 @@ const createRepo = async (templateUrl, ghToken, siteName) => {
return data
}

module.exports = { getTemplatesFromGitHub, createRepo }
module.exports = { getTemplatesFromGitHub, createRepo, validateTemplate }
18 changes: 9 additions & 9 deletions tests/integration/140.command.sites.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,27 @@ 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')

/* 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 = {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand All @@ -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: {
Expand All @@ -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) => {
Expand Down

1 comment on commit 4e74045

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📊 Benchmark results

Package size: 443 MB

Please sign in to comment.