1 | const Command = require('../utils/command')
|
2 | const openBrowser = require('../utils/open-browser')
|
3 | const path = require('path')
|
4 | const chalk = require('chalk')
|
5 | const { flags } = require('@oclif/command')
|
6 | const get = require('lodash.get')
|
7 | const fs = require('fs')
|
8 | const prettyjson = require('prettyjson')
|
9 | const ora = require('ora')
|
10 | const logSymbols = require('log-symbols')
|
11 | const cliSpinnerNames = Object.keys(require('cli-spinners'))
|
12 | const randomItem = require('random-item')
|
13 | const inquirer = require('inquirer')
|
14 | const SitesCreateCommand = require('./sites/create')
|
15 | const LinkCommand = require('./link')
|
16 |
|
17 | class 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 |
|
53 | if (initChoice === NEW_SITE) {
|
54 |
|
55 | siteData = await SitesCreateCommand.run([])
|
56 | site.id = siteData.id
|
57 | siteId = site.id
|
58 | } else if (initChoice === EXISTING_SITE) {
|
59 |
|
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 |
|
69 | this.error(e.message)
|
70 | }
|
71 | }
|
72 |
|
73 |
|
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 |
|
127 |
|
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 |
|
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 |
|
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 |
|
211 | this.log()
|
212 |
|
213 |
|
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 |
|
248 | DeployCommand.description = `Create a new deploy from the contents of a folder
|
249 |
|
250 | Deploys from the build settings found in the netlify.toml file, or settings from the API.
|
251 |
|
252 | The 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 |
|
257 | Lambda functions in the function folder can be in the following configurations for deployment:
|
258 |
|
259 |
|
260 | Built Go binaries:
|
261 | ------------------
|
262 |
|
263 | \`\`\`
|
264 | functions/
|
265 | └── nameOfGoFunction
|
266 | \`\`\`
|
267 |
|
268 | Build binaries of your Go language functions into the functions folder as part of your build process.
|
269 |
|
270 |
|
271 | Single file Node.js functions:
|
272 | -----------------------------
|
273 |
|
274 | Build 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 | \`\`\`
|
277 | functions/
|
278 | └── nameOfBundledNodeJSFunction.js
|
279 | \`\`\`
|
280 |
|
281 | Unbundled Node.js functions that have dependencies outside or inside of the functions folder:
|
282 | ---------------------------------------------------------------------------------------------
|
283 |
|
284 | You can ship unbundled Node.js functions with the CLI, utilizing top level project dependencies, or a nested package.json.
|
285 | If 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 | \`\`\`
|
288 | project/
|
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 |
|
300 | Any mix of these configurations works as well.
|
301 |
|
302 |
|
303 | Node.js function entry points
|
304 | -----------------------------
|
305 |
|
306 | Function entry points are determined by the file name and name of the folder they are in:
|
307 |
|
308 | \`\`\`
|
309 | functions/
|
310 | ├── aFolderlessFunctionEntrypoint.js
|
311 | └── functionName/
|
312 | ├── notTheEntryPoint.js
|
313 | └── functionName.js
|
314 | \`\`\`
|
315 |
|
316 | Support for package.json's main field, and intrinsic index.js entrypoints are coming soon.
|
317 | `
|
318 |
|
319 | DeployCommand.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 |
|
327 | DeployCommand.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 |
|
368 | function deployProgressCb() {
|
369 | const events = {}
|
370 | |
371 |
|
372 |
|
373 |
|
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 |
|
404 | function 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 |
|
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 |
|
430 | module.exports = DeployCommand
|