netlify-cli 16.8.0 → 16.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/npm-shrinkwrap.json +6423 -263
- package/package.json +3 -1
- package/src/commands/build/build.mjs +4 -2
- package/src/commands/deploy/deploy.mjs +11 -3
- package/src/commands/integration/deploy.mjs +397 -0
- package/src/commands/integration/index.mjs +25 -0
- package/src/commands/main.mjs +2 -0
- package/src/lib/functions/registry.mjs +31 -4
- package/src/lib/functions/server.mjs +15 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "netlify-cli",
|
|
3
3
|
"description": "Netlify command line tool",
|
|
4
|
-
"version": "16.
|
|
4
|
+
"version": "16.9.0",
|
|
5
5
|
"author": "Netlify Inc.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"@netlify/config": "20.9.0",
|
|
50
50
|
"@netlify/edge-bundler": "9.3.0",
|
|
51
51
|
"@netlify/local-functions-proxy": "1.1.1",
|
|
52
|
+
"@netlify/sdk": "^1.1.5",
|
|
52
53
|
"@netlify/serverless-functions-api": "1.9.1",
|
|
53
54
|
"@netlify/zip-it-and-ship-it": "9.25.1",
|
|
54
55
|
"@octokit/rest": "19.0.13",
|
|
@@ -104,6 +105,7 @@
|
|
|
104
105
|
"is-stream": "3.0.0",
|
|
105
106
|
"is-wsl": "2.2.0",
|
|
106
107
|
"isexe": "2.0.0",
|
|
108
|
+
"js-yaml": "^4.1.0",
|
|
107
109
|
"jsonwebtoken": "9.0.1",
|
|
108
110
|
"jwt-decode": "3.1.2",
|
|
109
111
|
"lambda-local": "2.1.2",
|
|
@@ -9,9 +9,11 @@ import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs'
|
|
|
9
9
|
/**
|
|
10
10
|
* @param {import('../../lib/build.mjs').BuildConfig} options
|
|
11
11
|
*/
|
|
12
|
-
const checkOptions = ({ cachedConfig: { siteInfo = {} }, token }) => {
|
|
12
|
+
export const checkOptions = ({ cachedConfig: { siteInfo = {} }, token }) => {
|
|
13
13
|
if (!siteInfo.id) {
|
|
14
|
-
error(
|
|
14
|
+
error(
|
|
15
|
+
'Could not find the site ID. If your site is not on Netlify, please run `netlify init` or `netlify deploy` first. If it is, please run `netlify link`.',
|
|
16
|
+
)
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
if (!token) {
|
|
@@ -475,12 +475,13 @@ const bundleEdgeFunctions = async (options, command) => {
|
|
|
475
475
|
*
|
|
476
476
|
* @param {object} config
|
|
477
477
|
* @param {boolean} config.deployToProduction
|
|
478
|
+
* @param {boolean} config.isIntegrationDeploy If the user ran netlify integration:deploy instead of just netlify deploy
|
|
478
479
|
* @param {boolean} config.json If the result should be printed as json message
|
|
479
480
|
* @param {boolean} config.runBuildCommand If the build command should be run
|
|
480
481
|
* @param {object} config.results
|
|
481
482
|
* @returns {void}
|
|
482
483
|
*/
|
|
483
|
-
const printResults = ({ deployToProduction, json, results, runBuildCommand }) => {
|
|
484
|
+
const printResults = ({ deployToProduction, isIntegrationDeploy, json, results, runBuildCommand }) => {
|
|
484
485
|
const msgData = {
|
|
485
486
|
'Build logs': results.logsUrl,
|
|
486
487
|
'Function logs': results.functionLogsUrl,
|
|
@@ -518,7 +519,11 @@ const printResults = ({ deployToProduction, json, results, runBuildCommand }) =>
|
|
|
518
519
|
if (!deployToProduction) {
|
|
519
520
|
log()
|
|
520
521
|
log('If everything looks good on your draft URL, deploy it to your main site URL with the --prod flag.')
|
|
521
|
-
log(
|
|
522
|
+
log(
|
|
523
|
+
`${chalk.cyanBright.bold(
|
|
524
|
+
`netlify ${isIntegrationDeploy ? 'integration:' : ''}deploy${runBuildCommand ? ' --build' : ''} --prod`,
|
|
525
|
+
)}`,
|
|
526
|
+
)
|
|
522
527
|
log()
|
|
523
528
|
}
|
|
524
529
|
}
|
|
@@ -529,7 +534,7 @@ const printResults = ({ deployToProduction, json, results, runBuildCommand }) =>
|
|
|
529
534
|
* @param {import('commander').OptionValues} options
|
|
530
535
|
* @param {import('../base-command.mjs').default} command
|
|
531
536
|
*/
|
|
532
|
-
const deploy = async (options, command) => {
|
|
537
|
+
export const deploy = async (options, command) => {
|
|
533
538
|
const { workingDir } = command
|
|
534
539
|
const { api, site, siteInfo } = command.netlify
|
|
535
540
|
const alias = options.alias || options.branch
|
|
@@ -675,8 +680,11 @@ const deploy = async (options, command) => {
|
|
|
675
680
|
// @ts-ignore
|
|
676
681
|
await restoreConfig(configMutations, { buildDir: deployFolder, configPath, redirectsPath })
|
|
677
682
|
|
|
683
|
+
const isIntegrationDeploy = command.name() === 'integration:deploy'
|
|
684
|
+
|
|
678
685
|
printResults({
|
|
679
686
|
runBuildCommand: options.build,
|
|
687
|
+
isIntegrationDeploy,
|
|
680
688
|
json: options.json,
|
|
681
689
|
results,
|
|
682
690
|
deployToProduction,
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/* eslint-disable import/extensions */
|
|
2
|
+
import { resolve } from 'path'
|
|
3
|
+
import { exit, env } from 'process'
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line n/no-missing-import
|
|
6
|
+
import { getConfiguration } from '@netlify/sdk/cli-utils'
|
|
7
|
+
// eslint-disable-next-line n/no-unpublished-import
|
|
8
|
+
import fs from 'fs-extra'
|
|
9
|
+
import inquirer from 'inquirer'
|
|
10
|
+
import yaml from 'js-yaml'
|
|
11
|
+
import fetch from 'node-fetch'
|
|
12
|
+
|
|
13
|
+
import { getBuildOptions } from '../../lib/build.mjs'
|
|
14
|
+
import { getToken, chalk, log } from '../../utils/command-helpers.mjs'
|
|
15
|
+
import { getSiteInformation } from '../../utils/dev.mjs'
|
|
16
|
+
import { checkOptions } from '../build/build.mjs'
|
|
17
|
+
import { deploy as siteDeploy } from '../deploy/deploy.mjs'
|
|
18
|
+
|
|
19
|
+
function getIntegrationAPIUrl() {
|
|
20
|
+
return env.INTEGRATION_URL || 'https://api.netlifysdk.com'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function areScopesEqual(localScopes, remoteScopes) {
|
|
24
|
+
if (localScopes.length !== remoteScopes.length) {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return localScopes.every((scope) => remoteScopes.includes(scope))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function logScopeConfirmationMessage(localScopes, remoteScopes) {
|
|
32
|
+
log(chalk.yellow(`This integration is already registered. The current required scopes are:`))
|
|
33
|
+
for (const scope of remoteScopes) {
|
|
34
|
+
log(chalk.green(`- ${scope}`))
|
|
35
|
+
}
|
|
36
|
+
log(chalk.yellow('and will be updated to:'))
|
|
37
|
+
for (const scope of localScopes) {
|
|
38
|
+
log(chalk.green(`- ${scope}`))
|
|
39
|
+
}
|
|
40
|
+
log(chalk.yellow('if you continue. This will only affect future installations of the integration.'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatScopesToWrite(registeredIntegrationScopes) {
|
|
44
|
+
let scopesToWrite = {}
|
|
45
|
+
|
|
46
|
+
for (const scope of registeredIntegrationScopes) {
|
|
47
|
+
const [resource, permission] = scope.split(':')
|
|
48
|
+
if (resource === 'all') {
|
|
49
|
+
scopesToWrite = { all: true }
|
|
50
|
+
break
|
|
51
|
+
} else {
|
|
52
|
+
if (!scopesToWrite[resource]) {
|
|
53
|
+
scopesToWrite[resource] = []
|
|
54
|
+
}
|
|
55
|
+
scopesToWrite[resource].push(permission)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return scopesToWrite
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatScopesForRemote(scopes) {
|
|
62
|
+
const scopesToWrite = []
|
|
63
|
+
if (scopes.all) {
|
|
64
|
+
scopesToWrite.push('all')
|
|
65
|
+
} else {
|
|
66
|
+
const scopeResources = Object.keys(scopes)
|
|
67
|
+
scopeResources.forEach((resource) => {
|
|
68
|
+
const permissionsRequested = scopes[resource]
|
|
69
|
+
permissionsRequested.forEach((permission) => {
|
|
70
|
+
scopesToWrite.push(`${resource}:${permission}`)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
return scopesToWrite.join(',')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function verifyRequiredFieldsAreInConfig(name, description, scopes, integrationLevel) {
|
|
78
|
+
const missingFields = []
|
|
79
|
+
|
|
80
|
+
if (!name) {
|
|
81
|
+
missingFields.push('name')
|
|
82
|
+
}
|
|
83
|
+
if (!description) {
|
|
84
|
+
missingFields.push('description')
|
|
85
|
+
}
|
|
86
|
+
if (!scopes) {
|
|
87
|
+
missingFields.push('scopes')
|
|
88
|
+
}
|
|
89
|
+
if (!integrationLevel) {
|
|
90
|
+
missingFields.push('integrationLevel')
|
|
91
|
+
}
|
|
92
|
+
if (missingFields.length !== 0) {
|
|
93
|
+
log(
|
|
94
|
+
chalk.yellow(
|
|
95
|
+
`You are missing the following fields for the integration to be deployed: ${missingFields.join(
|
|
96
|
+
', ',
|
|
97
|
+
)}. Please add a these fields as an entry to the integration.yaml file and try again.`,
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
log(
|
|
101
|
+
chalk.yellow(
|
|
102
|
+
'For more information on the required fields, please see the documentation: https://ntl.fyi/create-private-integration',
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// eslint-disable-next-line max-params
|
|
111
|
+
export async function registerIntegration(workingDir, siteId, accountId, localIntegrationConfig, token) {
|
|
112
|
+
const { description, integrationLevel, name, scopes, slug } = localIntegrationConfig
|
|
113
|
+
log(chalk.yellow(`An integration associated with the site ID ${siteId} is not registered.`))
|
|
114
|
+
const registerPrompt = await inquirer.prompt([
|
|
115
|
+
{
|
|
116
|
+
type: 'confirm',
|
|
117
|
+
name: 'registerIntegration',
|
|
118
|
+
message: `Would you like to register this site as a private integration now?`,
|
|
119
|
+
default: false,
|
|
120
|
+
},
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
if (!registerPrompt.registerIntegration) {
|
|
124
|
+
log(
|
|
125
|
+
chalk.white(
|
|
126
|
+
"Cancelling deployment. Please run 'netlify int deploy' again when you are ready to register the integration.",
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
log(
|
|
130
|
+
chalk.white(
|
|
131
|
+
"You can also register the integration through the Netlify UI on the 'Integrations' > 'Create private integration' page",
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
exit(1)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!verifyRequiredFieldsAreInConfig(name, description, scopes, integrationLevel)) {
|
|
138
|
+
exit(1)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
log(chalk.white('Registering the integration...'))
|
|
142
|
+
|
|
143
|
+
const { body, statusCode } = await fetch(`${getIntegrationAPIUrl()}/${accountId}/integrations`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: {
|
|
146
|
+
'netlify-token': token,
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify({
|
|
149
|
+
name,
|
|
150
|
+
slug,
|
|
151
|
+
description,
|
|
152
|
+
hostSiteId: siteId,
|
|
153
|
+
scopes: formatScopesForRemote(scopes),
|
|
154
|
+
integrationLevel,
|
|
155
|
+
}),
|
|
156
|
+
}).then(async (res) => {
|
|
157
|
+
const response = await res.json()
|
|
158
|
+
return { body: response, statusCode: res.status }
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
if (statusCode !== 201) {
|
|
162
|
+
log(chalk.red(`There was an error registering the integration:`))
|
|
163
|
+
log()
|
|
164
|
+
log(chalk.red(`-----------------------------------------------`))
|
|
165
|
+
log(chalk.red(body.msg))
|
|
166
|
+
log(chalk.red(`-----------------------------------------------`))
|
|
167
|
+
log()
|
|
168
|
+
log(chalk.red(`Please try again. If the problem persists, please contact support.`))
|
|
169
|
+
exit(1)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
log(chalk.green(`Successfully registered the integration with the slug: ${body.slug}`))
|
|
173
|
+
|
|
174
|
+
const updatedIntegrationConfig = yaml.dump({
|
|
175
|
+
config: { name, description, slug: body.slug, scopes, integrationLevel },
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const filePath = resolve(workingDir, 'integration.yaml')
|
|
179
|
+
await fs.writeFile(filePath, updatedIntegrationConfig)
|
|
180
|
+
|
|
181
|
+
log(chalk.yellow('Your integration.yaml file has been updated. Please commit and push these changes.'))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// eslint-disable-next-line max-params
|
|
185
|
+
export async function updateIntegration(
|
|
186
|
+
workingDir,
|
|
187
|
+
options,
|
|
188
|
+
siteId,
|
|
189
|
+
accountId,
|
|
190
|
+
localIntegrationConfig,
|
|
191
|
+
token,
|
|
192
|
+
registeredIntegration,
|
|
193
|
+
) {
|
|
194
|
+
let { description, integrationLevel, name, scopes, slug } = localIntegrationConfig
|
|
195
|
+
|
|
196
|
+
let integrationSlug = slug
|
|
197
|
+
if (slug !== registeredIntegration.slug) {
|
|
198
|
+
// Update the project's integration.yaml file with the remote slug since that will
|
|
199
|
+
// be considered the source of truth and is a value that can't be edited by the user.
|
|
200
|
+
// Let the user know they need to commit and push the changes.
|
|
201
|
+
integrationSlug = registeredIntegration.slug
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!name) {
|
|
205
|
+
// Disabling this lint rule because the destructuring was not assigning the variable correct and leading to a bug
|
|
206
|
+
// eslint-disable-next-line prefer-destructuring
|
|
207
|
+
name = registeredIntegration.name
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!description) {
|
|
211
|
+
// eslint-disable-next-line prefer-destructuring
|
|
212
|
+
description = registeredIntegration.description
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!integrationLevel) {
|
|
216
|
+
// eslint-disable-next-line prefer-destructuring
|
|
217
|
+
integrationLevel = registeredIntegration.integrationLevel
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// This is returned as a comma separated string and will be easier to manage here as an array
|
|
221
|
+
const registeredIntegrationScopes = registeredIntegration.scopes.split(',')
|
|
222
|
+
|
|
223
|
+
const scopeResources = Object.keys(scopes)
|
|
224
|
+
let localScopes = []
|
|
225
|
+
|
|
226
|
+
if (scopeResources.includes('all')) {
|
|
227
|
+
localScopes = ['all']
|
|
228
|
+
} else {
|
|
229
|
+
scopeResources.forEach((resource) => {
|
|
230
|
+
const permissionsRequested = scopes[resource]
|
|
231
|
+
permissionsRequested.forEach((permission) => {
|
|
232
|
+
localScopes.push(`${resource}:${permission}`)
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!areScopesEqual(localScopes, registeredIntegrationScopes)) {
|
|
238
|
+
logScopeConfirmationMessage(localScopes, registeredIntegrationScopes)
|
|
239
|
+
|
|
240
|
+
const scopePrompt = await inquirer.prompt([
|
|
241
|
+
{
|
|
242
|
+
type: 'confirm',
|
|
243
|
+
name: 'updateScopes',
|
|
244
|
+
message: `Do you want to update the scopes?`,
|
|
245
|
+
default: false,
|
|
246
|
+
},
|
|
247
|
+
])
|
|
248
|
+
|
|
249
|
+
let scopesToWrite
|
|
250
|
+
if (scopePrompt.updateScopes) {
|
|
251
|
+
// Update the scopes in remote
|
|
252
|
+
scopesToWrite = scopes
|
|
253
|
+
const { statusCode, updateResponse } = await fetch(
|
|
254
|
+
`${getIntegrationAPIUrl()}/${accountId}/integrations/${integrationSlug}`,
|
|
255
|
+
{
|
|
256
|
+
method: 'PUT',
|
|
257
|
+
headers: {
|
|
258
|
+
'netlify-token': token,
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify({
|
|
261
|
+
name,
|
|
262
|
+
description,
|
|
263
|
+
hostSiteId: siteId,
|
|
264
|
+
scopes: localScopes.join(','),
|
|
265
|
+
integrationLevel,
|
|
266
|
+
}),
|
|
267
|
+
},
|
|
268
|
+
).then(async (res) => {
|
|
269
|
+
const response = await res.json()
|
|
270
|
+
return { updateResponse: response, statusCode: res.status }
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
if (statusCode !== 200) {
|
|
274
|
+
log(
|
|
275
|
+
chalk.red(`There was an error updating the integration: ${updateResponse}`),
|
|
276
|
+
chalk.red('Please try again. If the problem persists, please contact support.'),
|
|
277
|
+
)
|
|
278
|
+
exit(1)
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
const useRegisteredScopesPrompt = await inquirer.prompt([
|
|
282
|
+
{
|
|
283
|
+
type: 'confirm',
|
|
284
|
+
name: 'useRegisteredScopes',
|
|
285
|
+
message: `Do you want to save the scopes registered for your integration in your local configuration file?`,
|
|
286
|
+
default: false,
|
|
287
|
+
},
|
|
288
|
+
])
|
|
289
|
+
|
|
290
|
+
if (useRegisteredScopesPrompt.useRegisteredScopes) {
|
|
291
|
+
// Use the scopes that are already registered
|
|
292
|
+
log(chalk.white('Saving the currently registered scopes to the integration.yaml file.'))
|
|
293
|
+
scopesToWrite = formatScopesToWrite(registeredIntegrationScopes)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!useRegisteredScopesPrompt.useRegisteredScopes && options.prod) {
|
|
297
|
+
log(chalk.red('Unable to deploy your integration to production without updating the registered scopes.'))
|
|
298
|
+
exit(1)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const updatedIntegrationConfig = yaml.dump({
|
|
303
|
+
config: { name, description, slug: integrationSlug, scopes: scopesToWrite, integrationLevel },
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
const filePath = resolve(workingDir, 'integration.yaml')
|
|
307
|
+
await fs.writeFile(filePath, updatedIntegrationConfig)
|
|
308
|
+
|
|
309
|
+
log(chalk.yellow('Changes to the integration.yaml file are complete. Please commit and push these changes.'))
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The deploy command for Netlify Integrations
|
|
315
|
+
* @param {import('commander').OptionValues} options
|
|
316
|
+
* * @param {import('../base-command.mjs').default} command
|
|
317
|
+
*/
|
|
318
|
+
const deploy = async (options, command) => {
|
|
319
|
+
const { api, cachedConfig, site, siteInfo } = command.netlify
|
|
320
|
+
const { id: siteId } = site
|
|
321
|
+
const [token] = await getToken()
|
|
322
|
+
const workingDir = resolve(command.workingDir)
|
|
323
|
+
const buildOptions = await getBuildOptions({
|
|
324
|
+
cachedConfig,
|
|
325
|
+
packagePath: command.workspacePackage,
|
|
326
|
+
token,
|
|
327
|
+
options,
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
// Confirm that a site is linked and that the user is logged in
|
|
331
|
+
checkOptions(buildOptions)
|
|
332
|
+
|
|
333
|
+
const { description, integrationLevel, name, scopes, slug } = await getConfiguration()
|
|
334
|
+
const localIntegrationConfig = { name, description, scopes, slug, integrationLevel }
|
|
335
|
+
|
|
336
|
+
const { accountId } = await getSiteInformation({
|
|
337
|
+
api,
|
|
338
|
+
site,
|
|
339
|
+
siteInfo,
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
const { body: registeredIntegration, statusCode } = await fetch(
|
|
343
|
+
`${getIntegrationAPIUrl()}/${accountId}/integrations?site_id=${siteId}`,
|
|
344
|
+
{
|
|
345
|
+
headers: {
|
|
346
|
+
'netlify-token': token,
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
).then(async (res) => {
|
|
350
|
+
const body = await res.json()
|
|
351
|
+
return { body, statusCode: res.status }
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
// The integration is registered on the remote
|
|
355
|
+
statusCode === 200
|
|
356
|
+
? await updateIntegration(
|
|
357
|
+
workingDir,
|
|
358
|
+
options,
|
|
359
|
+
siteId,
|
|
360
|
+
accountId,
|
|
361
|
+
localIntegrationConfig,
|
|
362
|
+
token,
|
|
363
|
+
registeredIntegration,
|
|
364
|
+
)
|
|
365
|
+
: await registerIntegration(workingDir, siteId, accountId, localIntegrationConfig, token)
|
|
366
|
+
|
|
367
|
+
// Set the prod flag to true if the integration is being initially registered because we don't want the user
|
|
368
|
+
// to be in a weird state where the card is appearing in the integrations list but there's no production
|
|
369
|
+
// version of the integration deployed
|
|
370
|
+
options = statusCode === 200 ? options : { ...options, prod: true }
|
|
371
|
+
|
|
372
|
+
// Deploy the integration to that site
|
|
373
|
+
await siteDeploy(options, command)
|
|
374
|
+
|
|
375
|
+
log(
|
|
376
|
+
`${chalk.cyanBright.bold(
|
|
377
|
+
`Your integration has been deployed. Next step is to enable it for a team or site.`,
|
|
378
|
+
)} https://ntl.fyi/create-private-integration`,
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Creates the `netlify int deploy` command
|
|
384
|
+
* @param {import('../base-command.mjs').default} program
|
|
385
|
+
* @returns
|
|
386
|
+
*/
|
|
387
|
+
export const createDeployCommand = (program) =>
|
|
388
|
+
program
|
|
389
|
+
.command('integration:deploy')
|
|
390
|
+
.alias('int:deploy')
|
|
391
|
+
.description('Register, build, and deploy a private integration on Netlify')
|
|
392
|
+
.option('-p, --prod', 'Deploy to production', false)
|
|
393
|
+
.option('-b, --build', 'Build the integration', false)
|
|
394
|
+
.option('-a, --auth <token>', 'Netlify auth token to deploy with', env.NETLIFY_AUTH_TOKEN)
|
|
395
|
+
.option('-s, --site <name-or-id>', 'A site name or ID to deploy to', env.NETLIFY_SITE_ID)
|
|
396
|
+
.action(deploy)
|
|
397
|
+
/* eslint-enable import/extensions */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createDeployCommand } from './deploy.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The int command
|
|
5
|
+
* @param {import('commander').OptionValues} options
|
|
6
|
+
* @param {import('../base-command.mjs').default} command
|
|
7
|
+
*/
|
|
8
|
+
const integrations = (options, command) => {
|
|
9
|
+
command.help()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates the `netlify integration` command
|
|
14
|
+
* @param {import('../base-command.mjs').default} program
|
|
15
|
+
* @returns
|
|
16
|
+
*/
|
|
17
|
+
export const createIntegrationCommand = (program) => {
|
|
18
|
+
createDeployCommand(program)
|
|
19
|
+
|
|
20
|
+
return program
|
|
21
|
+
.command('integration')
|
|
22
|
+
.alias('int')
|
|
23
|
+
.description('Manage Netlify Integrations built with the Netlify SDK')
|
|
24
|
+
.action(integrations)
|
|
25
|
+
}
|
package/src/commands/main.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { createDevCommand } from './dev/index.mjs'
|
|
|
22
22
|
import { createEnvCommand } from './env/index.mjs'
|
|
23
23
|
import { createFunctionsCommand } from './functions/index.mjs'
|
|
24
24
|
import { createInitCommand } from './init/index.mjs'
|
|
25
|
+
import { createIntegrationCommand } from './integration/index.mjs'
|
|
25
26
|
import { createLinkCommand } from './link/index.mjs'
|
|
26
27
|
import { createLmCommand } from './lm/index.mjs'
|
|
27
28
|
import { createLoginCommand } from './login/index.mjs'
|
|
@@ -191,6 +192,7 @@ export const createMainCommand = () => {
|
|
|
191
192
|
createFunctionsCommand(program)
|
|
192
193
|
createRecipesCommand(program)
|
|
193
194
|
createInitCommand(program)
|
|
195
|
+
createIntegrationCommand(program)
|
|
194
196
|
createLinkCommand(program)
|
|
195
197
|
createLmCommand(program)
|
|
196
198
|
createLoginCommand(program)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
import { mkdir } from 'fs/promises'
|
|
2
|
+
import { mkdir, stat } from 'fs/promises'
|
|
3
3
|
import { createRequire } from 'module'
|
|
4
4
|
import { basename, extname, isAbsolute, join, resolve } from 'path'
|
|
5
5
|
import { env } from 'process'
|
|
@@ -39,6 +39,7 @@ export class FunctionsRegistry {
|
|
|
39
39
|
debug = false,
|
|
40
40
|
isConnected = false,
|
|
41
41
|
logLambdaCompat,
|
|
42
|
+
manifest,
|
|
42
43
|
projectRoot,
|
|
43
44
|
settings,
|
|
44
45
|
timeouts,
|
|
@@ -96,6 +97,14 @@ export class FunctionsRegistry {
|
|
|
96
97
|
* @type {boolean}
|
|
97
98
|
*/
|
|
98
99
|
this.logLambdaCompat = Boolean(logLambdaCompat)
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Contents of a `manifest.json` file that can be looked up when dealing
|
|
103
|
+
* with built functions.
|
|
104
|
+
*
|
|
105
|
+
* @type {object}
|
|
106
|
+
*/
|
|
107
|
+
this.manifest = manifest
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
checkTypesPackage() {
|
|
@@ -390,12 +399,30 @@ export class FunctionsRegistry {
|
|
|
390
399
|
FunctionsRegistry.logEvent('extracted', { func })
|
|
391
400
|
}
|
|
392
401
|
|
|
393
|
-
|
|
402
|
+
// If there's a manifest file, look up the function in order to extract
|
|
403
|
+
// the build data.
|
|
404
|
+
const manifestEntry = (this.manifest?.functions || []).find((manifestFunc) => manifestFunc.name === func.name)
|
|
405
|
+
|
|
406
|
+
func.buildData = manifestEntry?.buildData || {}
|
|
407
|
+
|
|
408
|
+
// When we look at an unzipped function, we don't know whether it uses
|
|
409
|
+
// the legacy entry file format (i.e. `[function name].js`) or the new
|
|
410
|
+
// one (i.e. `___netlify-entry-point.mjs`). Let's look for the new one
|
|
411
|
+
// and use it if it exists, otherwise use the old one.
|
|
412
|
+
try {
|
|
413
|
+
const v2EntryPointPath = join(unzippedDirectory, '___netlify-entry-point.mjs')
|
|
414
|
+
|
|
415
|
+
await stat(v2EntryPointPath)
|
|
416
|
+
|
|
417
|
+
func.mainFile = v2EntryPointPath
|
|
418
|
+
} catch {
|
|
419
|
+
func.mainFile = join(unzippedDirectory, `${func.name}.js`)
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
this.buildFunctionAndWatchFiles(func, !isReload)
|
|
394
423
|
}
|
|
395
424
|
|
|
396
425
|
this.functions.set(name, func)
|
|
397
|
-
|
|
398
|
-
this.buildFunctionAndWatchFiles(func, !isReload)
|
|
399
426
|
}
|
|
400
427
|
|
|
401
428
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { Buffer } from 'buffer'
|
|
3
3
|
import { promises as fs } from 'fs'
|
|
4
|
+
import path from 'path'
|
|
4
5
|
|
|
5
6
|
import express from 'express'
|
|
6
7
|
import expressLogging from 'express-logging'
|
|
@@ -261,6 +262,7 @@ export const startFunctionsServer = async (options) => {
|
|
|
261
262
|
options
|
|
262
263
|
const internalFunctionsDir = await getInternalFunctionsDir({ base: site.root })
|
|
263
264
|
const functionsDirectories = []
|
|
265
|
+
let manifest
|
|
264
266
|
|
|
265
267
|
// If the `loadDistFunctions` parameter is sent, the functions server will
|
|
266
268
|
// use the built functions created by zip-it-and-ship-it rather than building
|
|
@@ -270,6 +272,18 @@ export const startFunctionsServer = async (options) => {
|
|
|
270
272
|
|
|
271
273
|
if (distPath) {
|
|
272
274
|
functionsDirectories.push(distPath)
|
|
275
|
+
|
|
276
|
+
// When using built functions, read the manifest file so that we can
|
|
277
|
+
// extract metadata such as routes and API version.
|
|
278
|
+
try {
|
|
279
|
+
const manifestPath = path.join(distPath, 'manifest.json')
|
|
280
|
+
// eslint-disable-next-line unicorn/prefer-json-parse-buffer
|
|
281
|
+
const data = await fs.readFile(manifestPath, 'utf8')
|
|
282
|
+
|
|
283
|
+
manifest = JSON.parse(data)
|
|
284
|
+
} catch {
|
|
285
|
+
// no-op
|
|
286
|
+
}
|
|
273
287
|
}
|
|
274
288
|
} else {
|
|
275
289
|
// The order of the function directories matters. Rightmost directories take
|
|
@@ -297,6 +311,7 @@ export const startFunctionsServer = async (options) => {
|
|
|
297
311
|
debug,
|
|
298
312
|
isConnected: Boolean(siteUrl),
|
|
299
313
|
logLambdaCompat: isFeatureFlagEnabled('cli_log_lambda_compat', siteInfo),
|
|
314
|
+
manifest,
|
|
300
315
|
// functions always need to be inside the packagePath if set inside a monorepo
|
|
301
316
|
projectRoot: command.workingDir,
|
|
302
317
|
settings,
|