UNPKG

12.5 kBJavaScriptView Raw
1const Command = require('../utils/command')
2const openBrowser = require('../utils/open-browser')
3const path = require('path')
4const chalk = require('chalk')
5const { flags } = require('@oclif/command')
6const get = require('lodash.get')
7const fs = require('fs')
8const prettyjson = require('prettyjson')
9const ora = require('ora')
10const logSymbols = require('log-symbols')
11const cliSpinnerNames = Object.keys(require('cli-spinners'))
12const randomItem = require('random-item')
13const inquirer = require('inquirer')
14const SitesCreateCommand = require('./sites/create')
15const LinkCommand = require('./link')
16
17class DeployCommand extends Command {
18 async run() {
19 const { flags } = this.parse(DeployCommand)
20 const { api, site, config } = this.netlify
21
22 const deployToProduction = flags.prod
23 await this.authenticate(flags.auth)
24
25 await this.config.runHook('analytics', {
26 eventName: 'command',
27 payload: {
28 command: 'deploy',
29 open: flags.open,
30 prod: flags.prod,
31 json: flags.json
32 }
33 })
34
35 let siteId = flags.site || site.id
36 let siteData
37 if (!siteId) {
38 this.log("This folder isn't linked to a site yet")
39 const NEW_SITE = '+ Create & configure a new site'
40 const EXISTING_SITE = 'Link this directory to an existing site'
41
42 const initializeOpts = [EXISTING_SITE, NEW_SITE]
43
44 const { initChoice } = await inquirer.prompt([
45 {
46 type: 'list',
47 name: 'initChoice',
48 message: 'What would you like to do?',
49 choices: initializeOpts
50 }
51 ])
52 // create site or search for one
53 if (initChoice === NEW_SITE) {
54 // run site:create command
55 siteData = await SitesCreateCommand.run([])
56 site.id = siteData.id
57 siteId = site.id
58 } else if (initChoice === EXISTING_SITE) {
59 // run link command
60 siteData = await LinkCommand.run([], false)
61 site.id = siteData.id
62 siteId = site.id
63 }
64 } else {
65 try {
66 siteData = await api.getSite({ siteId })
67 } catch (e) {
68 // TODO specifically handle known cases (e.g. no account access)
69 this.error(e.message)
70 }
71 }
72
73 // TODO: abstract settings lookup
74 let deployFolder
75 if (flags['dir']) {
76 deployFolder = path.resolve(process.cwd(), flags['dir'])
77 } else if (get(config, 'build.publish')) {
78 deployFolder = path.resolve(site.root, get(config, 'build.publish'))
79 } else if (get(siteData, 'build_settings.dir')) {
80 deployFolder = path.resolve(site.root, get(siteData, 'build_settings.dir'))
81 }
82
83 let functionsFolder
84 // Support "functions" and "Functions"
85 const funcConfig = get(config, 'build.functions') || get(config, 'build.Functions')
86 if (flags['functions']) {
87 functionsFolder = path.resolve(process.cwd(), flags['functions'])
88 } else if (funcConfig) {
89 functionsFolder = path.resolve(site.root, funcConfig)
90 } else if (get(siteData, 'build_settings.functions_dir')) {
91 functionsFolder = path.resolve(site.root, get(siteData, 'build_settings.functions_dir'))
92 }
93
94 if (!deployFolder) {
95 this.log('Please provide a publish directory (e.g. "public" or "dist" or "."):')
96 this.log(process.cwd())
97 const { promptPath } = await inquirer.prompt([
98 {
99 type: 'input',
100 name: 'promptPath',
101 message: 'Publish directory',
102 default: '.',
103 filter: input => path.resolve(process.cwd(), input)
104 }
105 ])
106 deployFolder = promptPath
107 }
108
109 const pathInfo = {
110 'Deploy path': deployFolder
111 }
112
113 if (functionsFolder) {
114 pathInfo['Functions path'] = functionsFolder
115 }
116 let configPath
117 if (site.configPath) {
118 configPath = site.configPath
119 pathInfo['Configuration path'] = configPath
120 }
121 this.log(prettyjson.render(pathInfo))
122
123 ensureDirectory(deployFolder, this.exit)
124
125 if (functionsFolder) {
126 // we used to hard error if functions folder is specified but doesnt exist
127 // but this was too strict for onboarding. we can just log a warning.
128 let stat
129 try {
130 stat = fs.statSync(functionsFolder)
131 } catch (e) {
132 if (e.code === 'ENOENT') {
133 console.log(
134 `Functions folder "${functionsFolder}" specified but it doesn't exist! Will proceed without deploying functions`
135 )
136 functionsFolder = undefined
137 }
138 // Improve the message of permission errors
139 if (e.code === 'EACCES') {
140 console.log('Permission error when trying to access functions directory')
141 this.exit(1)
142 }
143 }
144 if (functionsFolder && !stat.isDirectory) {
145 console.log('Deploy target must be a directory')
146 this.exit(1)
147 }
148 }
149
150 let results
151 try {
152 if (deployToProduction) {
153 this.log('Deploying to live site URL...')
154 } else {
155 this.log('Deploying to draft URL...')
156 }
157
158 results = await api.deploy(siteId, deployFolder, {
159 configPath: configPath,
160 fnDir: functionsFolder,
161 statusCb: flags.json || flags.silent ? () => {} : deployProgressCb(),
162 draft: !deployToProduction,
163 message: flags.message,
164 deployTimeout: flags.timeout * 1000 || 1.2e6
165 })
166 } catch (e) {
167 switch (true) {
168 case e.name === 'JSONHTTPError': {
169 this.warn(`JSONHTTPError: ${e.json.message} ${e.status}`)
170 this.warn(`\n${JSON.stringify(e, null, ' ')}\n`)
171 this.error(e)
172 return
173 }
174 case e.name === 'TextHTTPError': {
175 this.warn(`TextHTTPError: ${e.status}`)
176 this.warn(`\n${e}\n`)
177 this.error(e)
178 return
179 }
180 case e.message && e.message.includes('Invalid filename'): {
181 this.warn(e.message)
182 this.error(e)
183 return
184 }
185 default: {
186 this.warn(`\n${JSON.stringify(e, null, ' ')}\n`)
187 this.error(e)
188 return
189 }
190 }
191 }
192 // cliUx.action.stop(`Finished deploy ${results.deployId}`)
193
194 const siteUrl = results.deploy.ssl_url || results.deploy.url
195 const deployUrl = get(results, 'deploy.deploy_ssl_url') || get(results, 'deploy.deploy_url')
196
197 const logsUrl = `${get(results, 'deploy.admin_url')}/deploys/${get(results, 'deploy.id')}`
198 const msgData = {
199 Logs: `${logsUrl}`,
200 'Unique Deploy URL': deployUrl
201 }
202
203 if (deployToProduction) {
204 msgData['Live URL'] = siteUrl
205 } else {
206 delete msgData['Unique Deploy URL']
207 msgData['Live Draft URL'] = deployUrl
208 }
209
210 // Spacer
211 this.log()
212
213 // Json response for piping commands
214 if (flags.json && results) {
215 const jsonData = {
216 name: results.deploy.deployId,
217 site_id: results.deploy.site_id,
218 site_name: results.deploy.name,
219 deploy_id: results.deployId,
220 deploy_url: deployUrl,
221 logs: logsUrl
222 }
223 if (deployToProduction) {
224 jsonData.url = siteUrl
225 }
226
227 this.logJson(jsonData)
228 return false
229 }
230
231 this.log(prettyjson.render(msgData))
232
233 if (!deployToProduction) {
234 this.log()
235 this.log('If everything looks good on your draft URL, take it live with the --prod flag.')
236 this.log(`${chalk.cyanBright.bold('netlify deploy --prod')}`)
237 this.log()
238 }
239
240 if (flags['open']) {
241 const urlToOpen = flags['prod'] ? siteUrl : deployUrl
242 await openBrowser(urlToOpen)
243 this.exit()
244 }
245 }
246}
247
248DeployCommand.description = `Create a new deploy from the contents of a folder
249
250Deploys from the build settings found in the netlify.toml file, or settings from the API.
251
252The following environment variables can be used to override configuration file lookups and prompts:
253
254- \`NETLIFY_AUTH_TOKEN\` - an access token to use when authenticating commands. Keep this value private.
255- \`NETLIFY_SITE_ID\` - override any linked site in the current working directory.
256
257Lambda functions in the function folder can be in the following configurations for deployment:
258
259
260Built Go binaries:
261------------------
262
263\`\`\`
264functions/
265└── nameOfGoFunction
266\`\`\`
267
268Build binaries of your Go language functions into the functions folder as part of your build process.
269
270
271Single file Node.js functions:
272-----------------------------
273
274Build dependency bundled Node.js lambda functions with tools like netlify-lambda, webpack or browserify into the function folder as part of your build process.
275
276\`\`\`
277functions/
278└── nameOfBundledNodeJSFunction.js
279\`\`\`
280
281Unbundled Node.js functions that have dependencies outside or inside of the functions folder:
282---------------------------------------------------------------------------------------------
283
284You can ship unbundled Node.js functions with the CLI, utilizing top level project dependencies, or a nested package.json.
285If you use nested dependencies, be sure to populate the nested node_modules as part of your build process before deploying using npm or yarn.
286
287\`\`\`
288project/
289├── functions
290│ ├── functionName/
291│ │ ├── functionName.js (Note the folder and the function name need to match)
292│ │ ├── package.json
293│ │ └── node_modules/
294│ └── unbundledFunction.js
295├── package.json
296├── netlify.toml
297└── node_modules/
298\`\`\`
299
300Any mix of these configurations works as well.
301
302
303Node.js function entry points
304-----------------------------
305
306Function entry points are determined by the file name and name of the folder they are in:
307
308\`\`\`
309functions/
310├── aFolderlessFunctionEntrypoint.js
311└── functionName/
312 ├── notTheEntryPoint.js
313 └── functionName.js
314\`\`\`
315
316Support for package.json's main field, and intrinsic index.js entrypoints are coming soon.
317`
318
319DeployCommand.examples = [
320 'netlify deploy',
321 'netlify deploy --prod',
322 'netlify deploy --prod --open',
323 'netlify deploy --message "A message with an $ENV_VAR"',
324 'netlify deploy --auth $NETLIFY_AUTH_TOKEN'
325]
326
327DeployCommand.flags = {
328 dir: flags.string({
329 char: 'd',
330 description: 'Specify a folder to deploy'
331 }),
332 functions: flags.string({
333 char: 'f',
334 description: 'Specify a functions folder to deploy'
335 }),
336 prod: flags.boolean({
337 char: 'p',
338 description: 'Deploy to production',
339 default: false
340 }),
341 open: flags.boolean({
342 char: 'o',
343 description: 'Open site after deploy',
344 default: false
345 }),
346 message: flags.string({
347 char: 'm',
348 description: 'A short message to include in the deploy log'
349 }),
350 auth: flags.string({
351 char: 'a',
352 description: 'Netlify auth token to deploy with',
353 env: 'NETLIFY_AUTH_TOKEN'
354 }),
355 site: flags.string({
356 char: 's',
357 description: 'A site ID to deploy to',
358 env: 'NETLIFY_SITE_ID'
359 }),
360 json: flags.boolean({
361 description: 'Output deployment data as JSON'
362 }),
363 timeout: flags.integer({
364 description: 'Timeout to wait for deployment to finish'
365 })
366}
367
368function deployProgressCb() {
369 const events = {}
370 /* statusObj: {
371 type: name-of-step
372 msg: msg to print
373 phase: [start, progress, stop]
374 }
375 */
376 return ev => {
377 switch (ev.phase) {
378 case 'start': {
379 const spinner = ev.spinner || randomItem(cliSpinnerNames)
380 events[ev.type] = ora({
381 text: ev.msg,
382 spinner: spinner
383 }).start()
384 return
385 }
386 case 'progress': {
387 const spinner = events[ev.type]
388 if (spinner) spinner.text = ev.msg
389 return
390 }
391 case 'stop':
392 default: {
393 const spinner = events[ev.type]
394 if (spinner) {
395 spinner.stopAndPersist({ text: ev.msg, symbol: logSymbols.success })
396 delete events[ev.type]
397 }
398 return
399 }
400 }
401 }
402}
403
404function ensureDirectory(resolvedDeployPath, exit) {
405 let stat
406 try {
407 stat = fs.statSync(resolvedDeployPath)
408 } catch (e) {
409 if (e.code === 'ENOENT') {
410 console.log(
411 `No such directory ${resolvedDeployPath}! Did you forget to create a functions folder or run a build?`
412 )
413 exit(1)
414 }
415
416 // Improve the message of permission errors
417 if (e.code === 'EACCES') {
418 console.log('Permission error when trying to access deploy folder')
419 exit(1)
420 }
421 throw e
422 }
423 if (!stat.isDirectory) {
424 console.log('Deploy target must be a directory')
425 exit(1)
426 }
427 return stat
428}
429
430module.exports = DeployCommand