Skip to content

Commit 89be8a6

Browse files
benhancockdylanspyerDanielSLew
authored
feat(sites-create-template): auto-link cloned repo to the created netlify site (#6914)
Co-authored-by: Dylan Spyer <[email protected]> Co-authored-by: Daniel Lew <[email protected]>
1 parent fdaabc0 commit 89be8a6

File tree

3 files changed

+167
-3
lines changed

3 files changed

+167
-3
lines changed

src/commands/sites/sites-create-template.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import inquirer from 'inquirer'
33
import pick from 'lodash/pick.js'
44
import { render } from 'prettyjson'
55
import { v4 as uuid } from 'uuid'
6+
import path from 'node:path'
7+
import { fileURLToPath } from 'node:url'
68

79
import {
810
chalk,
@@ -20,7 +22,7 @@ import getRepoData from '../../utils/get-repo-data.js'
2022
import { getGitHubToken } from '../../utils/init/config-github.js'
2123
import { configureRepo } from '../../utils/init/config.js'
2224
import { deployedSiteExists, getGitHubLink, getTemplateName } from '../../utils/sites/create-template.js'
23-
import { createRepo, validateTemplate } from '../../utils/sites/utils.js'
25+
import { callLinkSite, createRepo, validateTemplate } from '../../utils/sites/utils.js'
2426
import { track } from '../../utils/telemetry/index.js'
2527
import { Account, SiteInfo } from '../../utils/types.js'
2628
import BaseCommand from '../base-command.js'
@@ -190,6 +192,58 @@ export const sitesCreateTemplate = async (repository: string, options: OptionVal
190192
}
191193

192194
log(`🚀 Repository cloned successfully. You can find it under the ${chalk.magenta(repoResp.name)} folder`)
195+
196+
const { linkConfirm } = await inquirer.prompt({
197+
type: 'confirm',
198+
name: 'linkConfirm',
199+
message: `Do you want to link the cloned directory to the site?`,
200+
default: true,
201+
})
202+
203+
if (linkConfirm) {
204+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
205+
206+
const cliPath = path.resolve(__dirname, '../../../bin/run.js')
207+
208+
let stdout
209+
if (repoResp.name) {
210+
stdout = await callLinkSite(cliPath, repoResp.name, '\n')
211+
} else {
212+
error()
213+
return
214+
}
215+
216+
const linkedSiteUrlRegex = /Site url:\s+(\S+)/
217+
const lineMatch = linkedSiteUrlRegex.exec(stdout)
218+
const urlMatch = lineMatch ? lineMatch[1] : undefined
219+
if (urlMatch) {
220+
log(`\nDirectory ${chalk.cyanBright(repoResp.name)} linked to site ${chalk.cyanBright(urlMatch)}\n`)
221+
log(
222+
`${chalk.cyanBright.bold('cd', repoResp.name)} to use other netlify cli commands in the cloned directory.\n`,
223+
)
224+
} else {
225+
const linkedSiteMatch = /Site already linked to\s+(\S+)/.exec(stdout)
226+
const linkedSiteNameMatch = linkedSiteMatch ? linkedSiteMatch[1] : undefined
227+
if (linkedSiteNameMatch) {
228+
log(`\nThis directory appears to be linked to ${chalk.cyanBright(linkedSiteNameMatch)}`)
229+
log('This can happen if you cloned the template into a subdirectory of an existing Netlify project.')
230+
log(
231+
`You may need to move the ${chalk.cyanBright(
232+
repoResp.name,
233+
)} directory out of its parent directory and then re-run the ${chalk.cyanBright(
234+
'link',
235+
)} command manually\n`,
236+
)
237+
} else {
238+
log('A problem occurred linking the site')
239+
log('You can try again manually by running:')
240+
log(chalk.cyanBright(`cd ${repoResp.name} && netlify link\n`))
241+
}
242+
}
243+
} else {
244+
log('To link the cloned directory manually, run:')
245+
log(chalk.cyanBright(`cd ${repoResp.name} && netlify link\n`))
246+
}
193247
}
194248

195249
if (options.withCi) {

src/utils/sites/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fetch from 'node-fetch'
2+
import execa from 'execa'
23

34
import { log, GitHubRepoResponse, error } from '../command-helpers.js'
45
import { GitHubRepo, Template } from '../types.js'
@@ -68,3 +69,11 @@ export const createRepo = async (
6869

6970
return data as GitHubRepoResponse
7071
}
72+
73+
export const callLinkSite = async (cliPath: string, repoName: string, input: string) => {
74+
const { stdout } = await execa(cliPath, ['link'], {
75+
input,
76+
cwd: repoName,
77+
})
78+
return stdout
79+
}

tests/integration/commands/sites/sites-create-template.test.ts

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ import { beforeEach, afterEach, describe, expect, test, vi, afterAll } from 'vit
66
import BaseCommand from '../../../../src/commands/base-command.ts'
77
import { createSitesFromTemplateCommand } from '../../../../src/commands/sites/sites.ts'
88
import { deployedSiteExists, fetchTemplates, getTemplateName } from '../../../../src/utils/sites/create-template.ts'
9-
import { getTemplatesFromGitHub, validateTemplate, createRepo } from '../../../../src/utils/sites/utils.ts'
9+
import {
10+
getTemplatesFromGitHub,
11+
validateTemplate,
12+
createRepo,
13+
callLinkSite,
14+
} from '../../../../src/utils/sites/utils.ts'
1015
import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js'
16+
import { chalk } from '../../../../src/utils/command-helpers.ts'
1117

1218
vi.mock('../../../../src/utils/init/config-github.ts')
1319
vi.mock('../../../../src/utils/sites/utils.ts')
@@ -49,13 +55,16 @@ describe('sites:create-template', () => {
4955
vi
5056
.fn()
5157
.mockImplementationOnce(() => Promise.resolve({ accountSlug: 'test-account' }))
52-
.mockImplementationOnce(() => Promise.resolve({ name: 'test-name' })),
58+
.mockImplementationOnce(() => Promise.resolve({ name: 'test-name' }))
59+
.mockImplementationOnce(() => Promise.resolve({ cloneConfirm: true }))
60+
.mockImplementationOnce(() => Promise.resolve({ linkConfirm: true })),
5361
{
5462
prompts: inquirer.prompt?.prompts || {},
5563
registerPrompt: inquirer.prompt?.registerPrompt || vi.fn(),
5664
restoreDefaultPrompts: inquirer.prompt?.restoreDefaultPrompts || vi.fn(),
5765
},
5866
)
67+
5968
vi.mocked(fetchTemplates).mockResolvedValue([
6069
{
6170
name: 'mockTemplateName',
@@ -82,6 +91,7 @@ describe('sites:create-template', () => {
8291
full_name: 'mockName',
8392
private: true,
8493
default_branch: 'mockBranch',
94+
name: 'repoName',
8595
})
8696
})
8797

@@ -145,4 +155,95 @@ describe('sites:create-template', () => {
145155
})
146156
expect(stdoutwriteSpy).toHaveBeenCalledWith('A site with that name already exists on your account\n')
147157
})
158+
159+
test('it should automatically link to the site when the user clones the template repo', async (t) => {
160+
const mockSuccessfulLinkOutput = `
161+
Directory Linked
162+
163+
Admin url: https://app.netlify.com/sites/site-name
164+
Site url: https://site-name.netlify.app
165+
166+
You can now run other \`netlify\` cli commands in this directory
167+
`
168+
vi.mocked(callLinkSite).mockImplementationOnce(() => Promise.resolve(mockSuccessfulLinkOutput))
169+
170+
const autoLinkRoutes = [
171+
{
172+
path: 'accounts',
173+
response: [{ slug: 'test-account' }],
174+
},
175+
{
176+
path: 'sites',
177+
response: [{ name: 'test-name-unique' }],
178+
},
179+
{
180+
path: 'test-account/sites',
181+
response: siteInfo,
182+
method: 'post',
183+
},
184+
]
185+
186+
const stdoutwriteSpy = vi.spyOn(process.stdout, 'write')
187+
await withMockApi(autoLinkRoutes, async ({ apiUrl }) => {
188+
Object.assign(process.env, getEnvironmentVariables({ apiUrl }))
189+
190+
const program = new BaseCommand('netlify')
191+
192+
vi.mocked(deployedSiteExists).mockResolvedValue(false)
193+
194+
createSitesFromTemplateCommand(program)
195+
196+
await program.parseAsync(['', '', 'sites:create-template'])
197+
})
198+
199+
expect(stdoutwriteSpy).toHaveBeenCalledWith(
200+
`\nDirectory ${chalk.cyanBright('repoName')} linked to site ${chalk.cyanBright(
201+
'https://site-name.netlify.app',
202+
)}\n\n`,
203+
)
204+
})
205+
206+
test('it should output instructions if a site is already linked', async (t) => {
207+
const mockUnsuccessfulLinkOutput = `
208+
Site already linked to \"site-name\"
209+
Admin url: https://app.netlify.com/sites/site-name
210+
211+
To unlink this site, run: netlify unlink
212+
`
213+
214+
vi.mocked(callLinkSite).mockImplementationOnce(() => Promise.resolve(mockUnsuccessfulLinkOutput))
215+
216+
const autoLinkRoutes = [
217+
{
218+
path: 'accounts',
219+
response: [{ slug: 'test-account' }],
220+
},
221+
{
222+
path: 'sites',
223+
response: [{ name: 'test-name-unique' }],
224+
},
225+
{
226+
path: 'test-account/sites',
227+
response: siteInfo,
228+
method: 'post',
229+
},
230+
]
231+
232+
const stdoutwriteSpy = vi.spyOn(process.stdout, 'write')
233+
await withMockApi(autoLinkRoutes, async ({ apiUrl }) => {
234+
Object.assign(process.env, getEnvironmentVariables({ apiUrl }))
235+
236+
const program = new BaseCommand('netlify')
237+
238+
vi.mocked(deployedSiteExists).mockResolvedValue(false)
239+
240+
createSitesFromTemplateCommand(program)
241+
242+
await program.parseAsync(['', '', 'sites:create-template'])
243+
})
244+
245+
expect(stdoutwriteSpy).toHaveBeenCalledWith(
246+
`\nThis directory appears to be linked to ${chalk.cyanBright(`"site-name"`)}\n`,
247+
)
248+
})
148249
})

0 commit comments

Comments
 (0)