netlify-cli 9.4.4 → 9.6.2

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "9.4.4",
3
+ "version": "9.6.2",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "netlify-cli",
9
- "version": "9.4.4",
9
+ "version": "9.6.2",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -15,7 +15,7 @@
15
15
  "@netlify/framework-info": "^9.0.0",
16
16
  "@netlify/local-functions-proxy": "^1.1.1",
17
17
  "@netlify/plugin-edge-handlers": "^3.0.6",
18
- "@netlify/plugins-list": "^6.10.2",
18
+ "@netlify/plugins-list": "^6.11.0",
19
19
  "@netlify/routing-local-proxy": "^0.34.1",
20
20
  "@netlify/zip-it-and-ship-it": "^5.7.5",
21
21
  "@octokit/rest": "^18.0.0",
@@ -4132,9 +4132,9 @@
4132
4132
  }
4133
4133
  },
4134
4134
  "node_modules/@netlify/plugins-list": {
4135
- "version": "6.10.2",
4136
- "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.10.2.tgz",
4137
- "integrity": "sha512-Agkt26O+bBQq3iXq7tSnDqWn+Ew3M7ImtoBNy/KI2KhNGaW1Mu31F9sIJqxvTUjVjBfklSI/L9M5GBWVAgk08A==",
4135
+ "version": "6.11.0",
4136
+ "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.11.0.tgz",
4137
+ "integrity": "sha512-b6+htKVooVnUPbq2zlCPUcWJSvR18a20cl1W3MS3EThEbXM/CCQWcCEqjwk+Q5VCH2hoJoaHlLhO/lIHpwn8Ig==",
4138
4138
  "engines": {
4139
4139
  "node": "^12.20.0 || ^14.14.0 || >=16.0.0"
4140
4140
  }
@@ -26586,9 +26586,9 @@
26586
26586
  }
26587
26587
  },
26588
26588
  "@netlify/plugins-list": {
26589
- "version": "6.10.2",
26590
- "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.10.2.tgz",
26591
- "integrity": "sha512-Agkt26O+bBQq3iXq7tSnDqWn+Ew3M7ImtoBNy/KI2KhNGaW1Mu31F9sIJqxvTUjVjBfklSI/L9M5GBWVAgk08A=="
26589
+ "version": "6.11.0",
26590
+ "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.11.0.tgz",
26591
+ "integrity": "sha512-b6+htKVooVnUPbq2zlCPUcWJSvR18a20cl1W3MS3EThEbXM/CCQWcCEqjwk+Q5VCH2hoJoaHlLhO/lIHpwn8Ig=="
26592
26592
  },
26593
26593
  "@netlify/routing-local-proxy": {
26594
26594
  "version": "0.34.1",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "9.4.4",
4
+ "version": "9.6.2",
5
5
  "author": "Netlify Inc.",
6
6
  "contributors": [
7
7
  "Abraham Schilling <AbrahamSchilling@gmail.com> (https://gitlab.com/n4bb12)",
@@ -204,7 +204,7 @@
204
204
  "@netlify/framework-info": "^9.0.0",
205
205
  "@netlify/local-functions-proxy": "^1.1.1",
206
206
  "@netlify/plugin-edge-handlers": "^3.0.6",
207
- "@netlify/plugins-list": "^6.10.2",
207
+ "@netlify/plugins-list": "^6.11.0",
208
208
  "@netlify/routing-local-proxy": "^0.34.1",
209
209
  "@netlify/zip-it-and-ship-it": "^5.7.5",
210
210
  "@octokit/rest": "^18.0.0",
@@ -1,4 +1,5 @@
1
1
  // @ts-check
2
+ const events = require('events')
2
3
  const process = require('process')
3
4
  const { format } = require('util')
4
5
 
@@ -28,6 +29,7 @@ const {
28
29
  pollForToken,
29
30
  sortOptions,
30
31
  track,
32
+ watchDebounced,
31
33
  } = require('../utils')
32
34
 
33
35
  // Netlify CLI client id. Lives in bot@netlify.com
@@ -119,6 +121,11 @@ class BaseCommand extends Command {
119
121
  await this.init(actionCommand)
120
122
  debug(`${name}:preAction`)('end')
121
123
  })
124
+ .hook('postAction', async () => {
125
+ if (this.configWatcherHandle) {
126
+ await this.configWatcherHandle.close()
127
+ }
128
+ })
122
129
  )
123
130
  }
124
131
 
@@ -430,6 +437,24 @@ class BaseCommand extends Command {
430
437
  const globalConfig = await getGlobalConfig()
431
438
  const { NetlifyAPI } = await jsClient
432
439
 
440
+ const configWatcher = new events.EventEmitter()
441
+
442
+ // Only set up a watcher if we know the config path.
443
+ if (configPath) {
444
+ const configWatcherHandle = await watchDebounced(configPath, {
445
+ depth: 1,
446
+ onChange: async () => {
447
+ const { config: newConfig } = await actionCommand.getConfig({ cwd, state, token, ...apiUrlOpts })
448
+
449
+ const normalizedNewConfig = normalizeConfig(newConfig)
450
+ configWatcher.emit('change', normalizedNewConfig)
451
+ },
452
+ })
453
+
454
+ // chokidar handler
455
+ this.configWatcherHandle = configWatcherHandle
456
+ }
457
+
433
458
  actionCommand.netlify = {
434
459
  // api methods
435
460
  api: new NetlifyAPI(token || '', apiOpts),
@@ -455,6 +480,8 @@ class BaseCommand extends Command {
455
480
  globalConfig,
456
481
  // state of current site dir
457
482
  state,
483
+ // netlify.toml file watcher
484
+ configWatcher,
458
485
  }
459
486
  debug(`${this.name()}:init`)('end')
460
487
  }
@@ -14,6 +14,7 @@ const { startFunctionsServer } = require('../../lib/functions/server')
14
14
  const {
15
15
  OneGraphCliClient,
16
16
  loadCLISession,
17
+ markCliSessionInactive,
17
18
  persistNewOperationsDocForSession,
18
19
  startOneGraphCLISession,
19
20
  } = require('../../lib/one-graph/cli-client')
@@ -32,6 +33,7 @@ const {
32
33
  detectServerSettings,
33
34
  error,
34
35
  exit,
36
+ generateNetlifyGraphJWT,
35
37
  getSiteInformation,
36
38
  injectEnvVariables,
37
39
  log,
@@ -78,6 +80,30 @@ const isNonExistingCommandError = ({ command, error: commandError }) => {
78
80
  )
79
81
  }
80
82
 
83
+ /**
84
+ * @type {(() => Promise<void>)[]} - array of functions to run before the process exits
85
+ */
86
+ const cleanupWork = []
87
+
88
+ let cleanupStarted = false
89
+
90
+ /**
91
+ * @param {object} input
92
+ * @param {number=} input.exitCode The exit code to return when exiting the process after cleanup
93
+ */
94
+ const cleanupBeforeExit = async ({ exitCode }) => {
95
+ // If cleanup has started, then wherever started it will be responsible for exiting
96
+ if (!cleanupStarted) {
97
+ cleanupStarted = true
98
+ try {
99
+ // eslint-disable-next-line no-unused-vars
100
+ const cleanupFinished = await Promise.all(cleanupWork.map((cleanup) => cleanup()))
101
+ } finally {
102
+ process.exit(exitCode)
103
+ }
104
+ }
105
+ }
106
+
81
107
  /**
82
108
  * Run a command and pipe stdout, stderr and stdin
83
109
  * @param {string} command
@@ -100,30 +126,29 @@ const runCommand = (command, env = {}) => {
100
126
 
101
127
  // we can't try->await->catch since we don't want to block on the framework server which
102
128
  // is a long running process
103
- // eslint-disable-next-line promise/catch-or-return,promise/prefer-await-to-then
104
- commandProcess.then(async () => {
105
- const result = await commandProcess
106
- const [commandWithoutArgs] = command.split(' ')
107
- // eslint-disable-next-line promise/always-return
108
- if (result.failed && isNonExistingCommandError({ command: commandWithoutArgs, error: result })) {
109
- log(
110
- NETLIFYDEVERR,
111
- `Failed running command: ${command}. Please verify ${chalk.magenta(`'${commandWithoutArgs}'`)} exists`,
112
- )
113
- } else {
114
- const errorMessage = result.failed
115
- ? `${NETLIFYDEVERR} ${result.shortMessage}`
116
- : `${NETLIFYDEVWARN} "${command}" exited with code ${result.exitCode}`
117
-
118
- log(`${errorMessage}. Shutting down Netlify Dev server`)
119
- }
120
- process.exit(1)
121
- })
122
- ;['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP', 'exit'].forEach((signal) => {
123
- process.on(signal, () => {
124
- commandProcess.kill('SIGTERM', { forceKillAfterTimeout: 500 })
125
- process.exit()
129
+ // eslint-disable-next-line promise/catch-or-return
130
+ commandProcess
131
+ // eslint-disable-next-line promise/prefer-await-to-then
132
+ .then(async () => {
133
+ const result = await commandProcess
134
+ const [commandWithoutArgs] = command.split(' ')
135
+ if (result.failed && isNonExistingCommandError({ command: commandWithoutArgs, error: result })) {
136
+ log(
137
+ NETLIFYDEVERR,
138
+ `Failed running command: ${command}. Please verify ${chalk.magenta(`'${commandWithoutArgs}'`)} exists`,
139
+ )
140
+ } else {
141
+ const errorMessage = result.failed
142
+ ? `${NETLIFYDEVERR} ${result.shortMessage}`
143
+ : `${NETLIFYDEVWARN} "${command}" exited with code ${result.exitCode}`
144
+
145
+ log(`${errorMessage}. Shutting down Netlify Dev server`)
146
+ }
147
+
148
+ return await cleanupBeforeExit({ exitCode: 1 })
126
149
  })
150
+ ;['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP', 'exit'].forEach((signal) => {
151
+ process.on(signal, async () => await cleanupBeforeExit({}))
127
152
  })
128
153
 
129
154
  return commandProcess
@@ -238,6 +263,44 @@ const printBanner = ({ url }) => {
238
263
  )
239
264
  }
240
265
 
266
+ const startPollingForAPIAuthentication = async function (options) {
267
+ const { api, command, config, site, siteInfo } = options
268
+ const frequency = 5000
269
+
270
+ const helper = async (maybeSiteData) => {
271
+ const siteData = await (maybeSiteData || api.getSite({ siteId: site.id }))
272
+ const authlifyTokenId = siteData && siteData.authlify_token_id
273
+
274
+ const existingAuthlifyTokenId = config && config.netlifyGraphConfig && config.netlifyGraphConfig.authlifyTokenId
275
+ if (authlifyTokenId && authlifyTokenId !== existingAuthlifyTokenId) {
276
+ const netlifyToken = await command.authenticate()
277
+ // Only inject the authlify config if a token ID exists. This prevents
278
+ // calling command.authenticate() (which opens a browser window) if the
279
+ // user hasn't enabled API Authentication
280
+ const netlifyGraphConfig = {
281
+ netlifyToken,
282
+ authlifyTokenId: siteData.authlify_token_id,
283
+ siteId: site.id,
284
+ }
285
+ config.netlifyGraphConfig = netlifyGraphConfig
286
+
287
+ const netlifyGraphJWT = generateNetlifyGraphJWT(netlifyGraphConfig)
288
+
289
+ if (netlifyGraphJWT != null) {
290
+ // XXX(anmonteiro): this name is deprecated. Delete after 3/31/2022
291
+ process.env.ONEGRAPH_AUTHLIFY_TOKEN = netlifyGraphJWT
292
+ process.env.NETLIFY_GRAPH_TOKEN = netlifyGraphJWT
293
+ }
294
+ } else {
295
+ delete config.netlifyGraphConfig
296
+ }
297
+
298
+ setTimeout(helper, frequency)
299
+ }
300
+
301
+ await helper(siteInfo)
302
+ }
303
+
241
304
  /**
242
305
  * The dev command
243
306
  * @param {import('commander').OptionValues} options
@@ -263,7 +326,6 @@ const dev = async (options, command) => {
263
326
  )
264
327
  }
265
328
 
266
- const startNetlifyGraphWatcher = Boolean(options.graph)
267
329
  await injectEnvVariables({ env: command.netlify.cachedConfig.env, site })
268
330
 
269
331
  const { addonsUrls, capabilities, siteUrl, timeouts } = await getSiteInformation({
@@ -285,11 +347,15 @@ const dev = async (options, command) => {
285
347
 
286
348
  command.setAnalyticsPayload({ projectType: settings.framework || 'custom', live: options.live })
287
349
 
350
+ const startNetlifyGraphWatcher = Boolean(options.graph)
351
+ if (startNetlifyGraphWatcher) {
352
+ startPollingForAPIAuthentication({ api, command, config, site, siteInfo })
353
+ }
354
+
288
355
  await startFunctionsServer({
289
356
  api,
290
357
  command,
291
358
  config,
292
- isGraphEnabled: startNetlifyGraphWatcher,
293
359
  settings,
294
360
  site,
295
361
  siteInfo,
@@ -322,26 +388,48 @@ const dev = async (options, command) => {
322
388
  } else if (startNetlifyGraphWatcher) {
323
389
  const netlifyToken = await command.authenticate()
324
390
  await OneGraphCliClient.ensureAppForSite(netlifyToken, site.id)
325
- const netlifyGraphConfig = await getNetlifyGraphConfig({ command, options, settings })
326
391
 
327
- let graphqlDocument = readGraphQLOperationsSourceFile(netlifyGraphConfig)
392
+ let stopWatchingCLISessions
328
393
 
329
- if (!graphqlDocument || graphqlDocument.trim().length === 0) {
330
- graphqlDocument = defaultExampleOperationsDoc
331
- }
394
+ const createOrResumeSession = async function () {
395
+ const netlifyGraphConfig = await getNetlifyGraphConfig({ command, options, settings })
332
396
 
333
- await startOneGraphCLISession({ netlifyGraphConfig, netlifyToken, site, state })
397
+ let graphqlDocument = readGraphQLOperationsSourceFile(netlifyGraphConfig)
334
398
 
335
- // Should be created by startOneGraphCLISession
336
- const oneGraphSessionId = loadCLISession(state)
399
+ if (!graphqlDocument || graphqlDocument.trim().length === 0) {
400
+ graphqlDocument = defaultExampleOperationsDoc
401
+ }
337
402
 
338
- await persistNewOperationsDocForSession({
339
- netlifyToken,
340
- oneGraphSessionId,
341
- operationsDoc: graphqlDocument,
342
- siteId: site.id,
403
+ stopWatchingCLISessions = await startOneGraphCLISession({ netlifyGraphConfig, netlifyToken, site, state })
404
+
405
+ // Should be created by startOneGraphCLISession
406
+ const oneGraphSessionId = loadCLISession(state)
407
+
408
+ await persistNewOperationsDocForSession({
409
+ netlifyGraphConfig,
410
+ netlifyToken,
411
+ oneGraphSessionId,
412
+ operationsDoc: graphqlDocument,
413
+ siteId: site.id,
414
+ siteRoot: site.root,
415
+ })
416
+
417
+ return oneGraphSessionId
418
+ }
419
+
420
+ //
421
+ // Set up a handler for config changes.
422
+ command.netlify.configWatcher.on('change', (newConfig) => {
423
+ command.netlify.config = newConfig
424
+ stopWatchingCLISessions()
425
+ createOrResumeSession()
343
426
  })
344
427
 
428
+ const oneGraphSessionId = await createOrResumeSession()
429
+ const cleanupSession = () => markCliSessionInactive({ netlifyToken, sessionId: oneGraphSessionId, siteId: site.id })
430
+
431
+ cleanupWork.push(cleanupSession)
432
+
345
433
  const graphEditUrl = getGraphEditUrlBySiteId({ siteId: site.id, oneGraphSessionId })
346
434
 
347
435
  log(
@@ -1,13 +1,7 @@
1
1
  // @ts-check
2
2
  const gitRepoInfo = require('git-repo-info')
3
3
 
4
- const {
5
- OneGraphCliClient,
6
- createCLISession,
7
- generateSessionName,
8
- loadCLISession,
9
- upsertMergeCLISessionMetadata,
10
- } = require('../../lib/one-graph/cli-client')
4
+ const { OneGraphCliClient, ensureCLISession, upsertMergeCLISessionMetadata } = require('../../lib/one-graph/cli-client')
11
5
  const {
12
6
  defaultExampleOperationsDoc,
13
7
  getGraphEditUrlBySiteId,
@@ -48,13 +42,12 @@ const graphEdit = async (options, command) => {
48
42
 
49
43
  await ensureAppForSite(netlifyToken, siteId)
50
44
 
51
- let oneGraphSessionId = loadCLISession(state)
52
- if (!oneGraphSessionId) {
53
- const sessionName = generateSessionName()
54
- const oneGraphSession = await createCLISession({ netlifyToken, siteId: site.id, sessionName, metadata: {} })
55
- state.set('oneGraphSessionId', oneGraphSession.id)
56
- oneGraphSessionId = state.get('oneGraphSessionId')
57
- }
45
+ const oneGraphSessionId = await ensureCLISession({
46
+ metadata: {},
47
+ netlifyToken,
48
+ site,
49
+ state,
50
+ })
58
51
 
59
52
  const { branch } = gitRepoInfo()
60
53
  const persistedDoc = await createPersistedQuery(netlifyToken, {
@@ -1,20 +1,28 @@
1
1
  // @ts-check
2
+ const inquirer = require('inquirer')
3
+ const { GraphQL } = require('netlify-onegraph-internal')
4
+
2
5
  const {
3
6
  buildSchema,
7
+ defaultExampleOperationsDoc,
8
+ extractFunctionsFromOperationDoc,
4
9
  generateHandlerByOperationName,
5
10
  getNetlifyGraphConfig,
11
+ readGraphQLOperationsSourceFile,
6
12
  readGraphQLSchemaFile,
7
13
  } = require('../../lib/one-graph/cli-netlify-graph')
8
14
  const { error, log } = require('../../utils')
9
15
 
16
+ const { parse } = GraphQL
17
+
10
18
  /**
11
19
  * Creates the `netlify graph:handler` command
12
- * @param {string} operationName
20
+ * @param {string} userOperationName
13
21
  * @param {import('commander').OptionValues} options
14
22
  * @param {import('../base-command').BaseCommand} command
15
23
  * @returns
16
24
  */
17
- const graphHandler = async (operationName, options, command) => {
25
+ const graphHandler = async (userOperationName, options, command) => {
18
26
  const netlifyGraphConfig = await getNetlifyGraphConfig({ command, options })
19
27
 
20
28
  const schemaString = readGraphQLSchemaFile(netlifyGraphConfig)
@@ -31,6 +39,64 @@ const graphHandler = async (operationName, options, command) => {
31
39
  error(`Failed to parse Netlify GraphQL schema`)
32
40
  }
33
41
 
42
+ let operationName = userOperationName
43
+ if (!operationName) {
44
+ try {
45
+ let currentOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
46
+ if (currentOperationsDoc.trim().length === 0) {
47
+ currentOperationsDoc = defaultExampleOperationsDoc
48
+ }
49
+
50
+ const parsedDoc = parse(currentOperationsDoc)
51
+ const { functions } = extractFunctionsFromOperationDoc(parsedDoc)
52
+
53
+ const sorted = Object.values(functions).sort((aItem, bItem) =>
54
+ aItem.operationName.localeCompare(bItem.operationName),
55
+ )
56
+
57
+ const perPage = 50
58
+
59
+ const allOperationChoices = sorted.map((operation) => ({
60
+ name: `${operation.operationName} (${operation.kind})`,
61
+ value: operation.operationName,
62
+ }))
63
+
64
+ const filterOperationNames = (operationChoices, input) =>
65
+ operationChoices.filter((operation) => operation.value.toLowerCase().match(input.toLowerCase()))
66
+
67
+ // eslint-disable-next-line node/global-require
68
+ const inquirerAutocompletePrompt = require('inquirer-autocomplete-prompt')
69
+ /** multiple matching detectors, make the user choose */
70
+ inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt)
71
+
72
+ const { selectedOperationName } = await inquirer.prompt({
73
+ name: 'selectedOperationName',
74
+ message: `For which operation would you like to generate a handler?`,
75
+ type: 'autocomplete',
76
+ pageSize: perPage,
77
+ source(_, input) {
78
+ if (!input || input === '') {
79
+ return allOperationChoices
80
+ }
81
+
82
+ const filteredChoices = filterOperationNames(allOperationChoices, input)
83
+ // only show filtered results
84
+ return filteredChoices
85
+ },
86
+ })
87
+
88
+ if (selectedOperationName) {
89
+ operationName = selectedOperationName
90
+ }
91
+ } catch (parseError) {
92
+ parseError(`Error parsing operations library: ${parseError}`)
93
+ }
94
+ }
95
+
96
+ if (!operationName) {
97
+ error(`No operation name provided`)
98
+ }
99
+
34
100
  generateHandlerByOperationName({ logger: log, netlifyGraphConfig, schema, operationName, handlerOptions: {} })
35
101
  }
36
102
 
@@ -42,7 +108,7 @@ const graphHandler = async (operationName, options, command) => {
42
108
  const createGraphHandlerCommand = (program) =>
43
109
  program
44
110
  .command('graph:handler')
45
- .argument('<name>', 'Operation name')
111
+ .argument('[name]', 'Operation name')
46
112
  .description(
47
113
  'Generate a handler for a Graph operation given its name. See `graph:operations` for a list of operations.',
48
114
  )
@@ -15,6 +15,6 @@
15
15
  "author": "Netlify",
16
16
  "license": "MIT",
17
17
  "dependencies": {
18
- "@netlify/functions": "^0.11.0"
18
+ "@netlify/functions": "^0.11.1"
19
19
  }
20
20
  }
@@ -9,15 +9,15 @@
9
9
  "version": "1.0.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "@netlify/functions": "^0.11.0",
13
- "@types/node": "^14.18.11",
12
+ "@netlify/functions": "^0.11.1",
13
+ "@types/node": "^14.0.0",
14
14
  "typescript": "^4.0.0"
15
15
  }
16
16
  },
17
17
  "node_modules/@netlify/functions": {
18
- "version": "0.11.0",
19
- "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-0.11.0.tgz",
20
- "integrity": "sha512-+WWX081UCkLZYkfh1Ru+PH5HTujNXugeskATGhZLTyS1U3Cqh2gYLArdG1a/vU8WWopu/wjpxEOdWq8/T5Nw5Q==",
18
+ "version": "0.11.1",
19
+ "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-0.11.1.tgz",
20
+ "integrity": "sha512-J2QUAYRblvTrl/cOYU2RNHdv/EYy4qnKQhds0aDNB560Y83wfpaMS3F7PKpHygGWrVuLRyevenIzsV9MaQrKlQ==",
21
21
  "dependencies": {
22
22
  "is-promise": "^4.0.0"
23
23
  },
@@ -50,9 +50,9 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@netlify/functions": {
53
- "version": "0.11.0",
54
- "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-0.11.0.tgz",
55
- "integrity": "sha512-+WWX081UCkLZYkfh1Ru+PH5HTujNXugeskATGhZLTyS1U3Cqh2gYLArdG1a/vU8WWopu/wjpxEOdWq8/T5Nw5Q==",
53
+ "version": "0.11.1",
54
+ "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-0.11.1.tgz",
55
+ "integrity": "sha512-J2QUAYRblvTrl/cOYU2RNHdv/EYy4qnKQhds0aDNB560Y83wfpaMS3F7PKpHygGWrVuLRyevenIzsV9MaQrKlQ==",
56
56
  "requires": {
57
57
  "is-promise": "^4.0.0"
58
58
  }
@@ -14,7 +14,7 @@
14
14
  "author": "Netlify",
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
- "@netlify/functions": "^0.11.0",
17
+ "@netlify/functions": "^0.11.1",
18
18
  "@types/node": "^14.0.0",
19
19
  "typescript": "^4.0.0"
20
20
  }
@@ -15,7 +15,7 @@
15
15
  "author": "Netlify",
16
16
  "license": "MIT",
17
17
  "dependencies": {
18
- "@netlify/functions": "^0.11.0",
18
+ "@netlify/functions": "^0.11.1",
19
19
  "@types/node": "^14.18.9",
20
20
  "typescript": "^4.5.5"
21
21
  }
@@ -5,12 +5,11 @@ const { env } = require('process')
5
5
 
6
6
  const terminalLink = require('terminal-link')
7
7
 
8
- const { NETLIFYDEVERR, NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, log, warn } = require('../../utils')
8
+ const { NETLIFYDEVERR, NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, log, warn, watchDebounced } = require('../../utils')
9
9
  const { getLogMessage } = require('../log')
10
10
 
11
11
  const { NetlifyFunction } = require('./netlify-function')
12
12
  const runtimes = require('./runtimes')
13
- const { watchDebounced } = require('./watcher')
14
13
 
15
14
  const ZIP_EXTENSION = '.zip'
16
15
 
@@ -1,6 +1,4 @@
1
1
  // @ts-check
2
- const process = require('process')
3
-
4
2
  const jwtDecode = require('jwt-decode')
5
3
 
6
4
  const {
@@ -47,51 +45,9 @@ const buildClientContext = function (headers) {
47
45
  }
48
46
  }
49
47
 
50
- const startPollingForAPIAuthentication = async function (options) {
51
- const { api, command, config, site, siteInfo } = options
52
- const frequency = 5000
53
-
54
- const helper = async (maybeSiteData) => {
55
- const siteData = await (maybeSiteData || api.getSite({ siteId: site.id }))
56
- const authlifyTokenId = siteData && siteData.authlify_token_id
57
-
58
- const existingAuthlifyTokenId = config && config.netlifyGraphConfig && config.netlifyGraphConfig.authlifyTokenId
59
- if (authlifyTokenId && authlifyTokenId !== existingAuthlifyTokenId) {
60
- const netlifyToken = await command.authenticate()
61
- // Only inject the authlify config if a token ID exists. This prevents
62
- // calling command.authenticate() (which opens a browser window) if the
63
- // user hasn't enabled API Authentication
64
- const netlifyGraphConfig = {
65
- netlifyToken,
66
- authlifyTokenId: siteData.authlify_token_id,
67
- siteId: site.id,
68
- }
69
- config.netlifyGraphConfig = netlifyGraphConfig
70
-
71
- const netlifyGraphJWT = generateNetlifyGraphJWT(netlifyGraphConfig)
72
-
73
- if (netlifyGraphJWT != null) {
74
- // XXX(anmonteiro): this name is deprecated. Delete after 3/31/2022
75
- process.env.ONEGRAPH_AUTHLIFY_TOKEN = netlifyGraphJWT
76
- process.env.NETLIFY_GRAPH_TOKEN = netlifyGraphJWT
77
- }
78
- } else {
79
- delete config.authlify
80
- }
81
-
82
- setTimeout(helper, frequency)
83
- }
84
-
85
- await helper(siteInfo)
86
- }
87
-
88
48
  const createHandler = function (options) {
89
49
  const { config, functionsRegistry } = options
90
50
 
91
- if (options.isGraphEnabled) {
92
- startPollingForAPIAuthentication(options)
93
- }
94
-
95
51
  return async function handler(request, response) {
96
52
  // handle proxies without path re-writes (http-servr)
97
53
  const cleanPath = request.path.replace(/^\/.netlify\/(functions|builders)/, '')
@@ -152,7 +108,7 @@ const createHandler = function (options) {
152
108
  rawQuery,
153
109
  }
154
110
 
155
- if (config && config.authlify && config.authlify.authlifyTokenId != null) {
111
+ if (config && config.netlifyGraphConfig && config.netlifyGraphConfig.authlifyTokenId != null) {
156
112
  // XXX(anmonteiro): this name is deprecated. Delete after 3/31/2022
157
113
  const jwt = generateNetlifyGraphJWT(config.netlifyGraphConfig)
158
114
  event.authlifyToken = jwt
@@ -10,8 +10,7 @@ const { GraphQL, InternalConsole, OneGraphClient } = require('netlify-onegraph-i
10
10
  const { NetlifyGraph } = require('netlify-onegraph-internal')
11
11
 
12
12
  // eslint-disable-next-line no-unused-vars
13
- const { StateConfig, USER_AGENT, chalk, error, log, warn } = require('../../utils')
14
- const { watchDebounced } = require('../functions/watcher')
13
+ const { StateConfig, USER_AGENT, chalk, error, log, warn, watchDebounced } = require('../../utils')
15
14
 
16
15
  const {
17
16
  generateFunctionsFile,
@@ -24,7 +23,13 @@ const {
24
23
 
25
24
  const { parse } = GraphQL
26
25
  const { defaultExampleOperationsDoc, extractFunctionsFromOperationDoc } = NetlifyGraph
27
- const { createPersistedQuery, ensureAppForSite, updateCLISessionMetadata } = OneGraphClient
26
+ const {
27
+ createPersistedQuery,
28
+ ensureAppForSite,
29
+ executeMarkCliSessionActiveHeartbeat,
30
+ executeMarkCliSessionInactive,
31
+ updateCLISessionMetadata,
32
+ } = OneGraphClient
28
33
 
29
34
  const internalConsole = {
30
35
  log,
@@ -51,13 +56,31 @@ InternalConsole.registerConsole(internalConsole)
51
56
  * @param {function} input.onEvents A function to call when CLI events are received and need to be processed
52
57
  * @param {string} input.sessionId The session id to monitor CLI events for
53
58
  * @param {StateConfig} input.state A function to call to set/get the current state of the local Netlify project
59
+ * @param {any} input.site The site object
54
60
  * @returns
55
61
  */
56
62
  const monitorCLISessionEvents = (input) => {
57
- const { appId, netlifyGraphConfig, netlifyToken, onClose, onError, onEvents, sessionId, state } = input
63
+ const { appId, netlifyGraphConfig, netlifyToken, onClose, onError, onEvents, sessionId, site, state } = input
58
64
 
59
65
  const frequency = 5000
66
+ // 30 minutes
67
+ const defaultHeartbeatFrequency = 1_800_000
60
68
  let shouldClose = false
69
+ let nextMarkActiveHeartbeat = defaultHeartbeatFrequency
70
+
71
+ const markActiveHelper = async () => {
72
+ const fullSession = await OneGraphClient.fetchCliSession({ authToken: netlifyToken, appId, sessionId })
73
+ // @ts-ignore
74
+ const heartbeatIntervalms = fullSession.session.cliHeartbeatIntervalMs || defaultHeartbeatFrequency
75
+ nextMarkActiveHeartbeat = heartbeatIntervalms
76
+ const markCLISessionActiveResult = await executeMarkCliSessionActiveHeartbeat(netlifyToken, site.id, sessionId)
77
+ if (markCLISessionActiveResult.errors && markCLISessionActiveResult.errors.length !== 0) {
78
+ warn(`Failed to mark CLI session active: ${markCLISessionActiveResult.errors.join(', ')}`)
79
+ }
80
+ setTimeout(markActiveHelper, nextMarkActiveHeartbeat)
81
+ }
82
+
83
+ setTimeout(markActiveHelper, nextMarkActiveHeartbeat)
61
84
 
62
85
  const enabledServiceWatcher = async (innerNetlifyToken, siteId) => {
63
86
  const enabledServices = state.get('oneGraphEnabledServices') || ['onegraph']
@@ -90,7 +113,7 @@ const monitorCLISessionEvents = (input) => {
90
113
  const helper = async () => {
91
114
  if (shouldClose) {
92
115
  clearTimeout(handle)
93
- onClose()
116
+ onClose && onClose()
94
117
  }
95
118
 
96
119
  const next = await OneGraphClient.fetchCliSessionEvents({ appId, authToken: netlifyToken, sessionId })
@@ -371,6 +394,7 @@ const upsertMergeCLISessionMetadata = async ({ netlifyToken, newMetadata, oneGra
371
394
 
372
395
  const detectedMetadata = detectLocalCLISessionMetadata({ siteRoot })
373
396
 
397
+ // @ts-ignore
374
398
  const finalMetadata = { ...metadata, ...detectedMetadata, ...newMetadata }
375
399
  return OneGraphClient.updateCLISessionMetadata(netlifyToken, siteId, oneGraphSessionId, finalMetadata)
376
400
  }
@@ -429,24 +453,18 @@ const loadCLISession = (state) => state.get('oneGraphSessionId')
429
453
  const startOneGraphCLISession = async (input) => {
430
454
  const { netlifyGraphConfig, netlifyToken, site, state } = input
431
455
  OneGraphClient.ensureAppForSite(netlifyToken, site.id)
432
- let oneGraphSessionId = loadCLISession(state)
433
- if (!oneGraphSessionId) {
434
- const sessionName = generateSessionName()
435
- const sessionMetadata = {}
436
- const oneGraphSession = await createCLISession({
437
- netlifyToken,
438
- siteId: site.id,
439
- sessionName,
440
- metadata: sessionMetadata,
441
- })
442
- state.set('oneGraphSessionId', oneGraphSession.id)
443
- oneGraphSessionId = state.get('oneGraphSessionId')
444
- }
456
+
457
+ const oneGraphSessionId = await ensureCLISession({
458
+ metadata: {},
459
+ netlifyToken,
460
+ site,
461
+ state,
462
+ })
445
463
 
446
464
  const enabledServices = []
447
465
  const schema = await OneGraphClient.fetchOneGraphSchema(site.id, enabledServices)
448
466
 
449
- monitorOperationFile({
467
+ const opsFileWatcher = monitorOperationFile({
450
468
  netlifyGraphConfig,
451
469
  onChange: async (filePath) => {
452
470
  log('NetlifyGraph operation file changed at', filePath, 'updating function library...')
@@ -476,11 +494,12 @@ const startOneGraphCLISession = async (input) => {
476
494
  },
477
495
  })
478
496
 
479
- monitorCLISessionEvents({
497
+ const cliEventsCloseFn = monitorCLISessionEvents({
480
498
  appId: site.id,
481
499
  netlifyToken,
482
500
  netlifyGraphConfig,
483
501
  sessionId: oneGraphSessionId,
502
+ site,
484
503
  state,
485
504
  onEvents: async (events) => {
486
505
  for (const event of events) {
@@ -494,10 +513,27 @@ const startOneGraphCLISession = async (input) => {
494
513
  onError: (fetchEventError) => {
495
514
  error(`Netlify Graph upstream error: ${fetchEventError}`)
496
515
  },
497
- onClose: () => {
498
- log('Netlify Graph upstream closed')
499
- },
500
516
  })
517
+
518
+ return async function unregisterWatchers() {
519
+ const watcher = await opsFileWatcher
520
+ watcher.close()
521
+ cliEventsCloseFn()
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Mark a session as inactive so it doesn't show up in any UI lists, and potentially becomes available to GC later
527
+ * @param {object} input
528
+ * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
529
+ * @param {string} input.siteId A function to call to set/get the current state of the local Netlify project
530
+ * @param {string} input.sessionId The session id to monitor CLI events for
531
+ */
532
+ const markCliSessionInactive = async ({ netlifyToken, sessionId, siteId }) => {
533
+ const result = await executeMarkCliSessionInactive(netlifyToken, siteId, sessionId)
534
+ if (result.errors) {
535
+ warn(`Unable to mark CLI session ${sessionId} inactive: ${JSON.stringify(result.errors, null, 2)}`)
536
+ }
501
537
  }
502
538
 
503
539
  /**
@@ -511,6 +547,70 @@ const generateSessionName = () => {
511
547
  return sessionName
512
548
  }
513
549
 
550
+ /**
551
+ * Ensures a cli session exists for the current checkout, or errors out if it doesn't and cannot create one.
552
+ */
553
+ const ensureCLISession = async ({ metadata, netlifyToken, site, state }) => {
554
+ let oneGraphSessionId = loadCLISession(state)
555
+ let parentCliSessionId = null
556
+
557
+ // Validate that session still exists and we can access it
558
+ try {
559
+ if (oneGraphSessionId) {
560
+ const sessionEvents = await OneGraphClient.fetchCliSessionEvents({
561
+ appId: site.id,
562
+ authToken: netlifyToken,
563
+ sessionId: oneGraphSessionId,
564
+ })
565
+ if (sessionEvents.errors) {
566
+ warn(`Unable to fetch cli session: ${JSON.stringify(sessionEvents.errors, null, 2)}`)
567
+ log(`Creating new cli session`)
568
+ parentCliSessionId = oneGraphSessionId
569
+ oneGraphSessionId = null
570
+ }
571
+ }
572
+ } catch (fetchSessionError) {
573
+ warn(`Unable to fetch cli session events: ${JSON.stringify(fetchSessionError, null, 2)}`)
574
+ oneGraphSessionId = null
575
+ }
576
+
577
+ if (!oneGraphSessionId) {
578
+ // If we can't access the session in the state.json or it doesn't exist, create a new one
579
+ const sessionName = generateSessionName()
580
+ const detectedMetadata = detectLocalCLISessionMetadata({ siteRoot: site.root })
581
+ const newSessionMetadata = parentCliSessionId ? { parentCliSessionId } : {}
582
+ const sessionMetadata = {
583
+ ...detectedMetadata,
584
+ ...newSessionMetadata,
585
+ ...metadata,
586
+ }
587
+ const oneGraphSession = await createCLISession({
588
+ netlifyToken,
589
+ siteId: site.id,
590
+ sessionName,
591
+ metadata: sessionMetadata,
592
+ })
593
+ state.set('oneGraphSessionId', oneGraphSession.id)
594
+ oneGraphSessionId = state.get('oneGraphSessionId')
595
+ }
596
+
597
+ if (!oneGraphSessionId) {
598
+ error('Unable to create or access Netlify Graph CLI session')
599
+ }
600
+
601
+ const { errors: markCLISessionActiveErrors } = await executeMarkCliSessionActiveHeartbeat(
602
+ netlifyToken,
603
+ site.id,
604
+ oneGraphSessionId,
605
+ )
606
+
607
+ if (markCLISessionActiveErrors) {
608
+ warn(`Unable to mark cli session active: ${JSON.stringify(markCLISessionActiveErrors, null, 2)}`)
609
+ }
610
+
611
+ return oneGraphSessionId
612
+ }
613
+
514
614
  const OneGraphCliClient = {
515
615
  ackCLISessionEvents: OneGraphClient.ackCLISessionEvents,
516
616
  createPersistedQuery,
@@ -522,10 +622,12 @@ const OneGraphCliClient = {
522
622
  module.exports = {
523
623
  OneGraphCliClient,
524
624
  createCLISession,
625
+ ensureCLISession,
525
626
  extractFunctionsFromOperationDoc,
526
627
  handleCliSessionEvent,
527
628
  generateSessionName,
528
629
  loadCLISession,
630
+ markCliSessionInactive,
529
631
  monitorCLISessionEvents,
530
632
  persistNewOperationsDocForSession,
531
633
  refetchAndGenerateFromOneGraph,
@@ -217,8 +217,7 @@ const getNetlifyGraphConfig = async ({ command, options, settings }) => {
217
217
  const moduleType =
218
218
  (userSpecifiedConfig.moduleType && userSpecifiedConfig.moduleType.split(path.sep)) ||
219
219
  defaultFrameworkConfig.moduleType
220
- const language =
221
- (userSpecifiedConfig.language && userSpecifiedConfig.language.split(path.sep)) || autodetectedLanguage
220
+ const language = userSpecifiedConfig.language || autodetectedLanguage
222
221
  const webhookBasePath =
223
222
  (userSpecifiedConfig.webhookBasePath && userSpecifiedConfig.webhookBasePath.split(path.sep)) ||
224
223
  defaultFrameworkConfig.webhookBasePath
@@ -4,8 +4,12 @@ const process = require('process')
4
4
  const { format, inspect } = require('util')
5
5
 
6
6
  const { Instance: ChalkInstance } = require('chalk')
7
+ const chokidar = require('chokidar')
8
+ const decache = require('decache')
7
9
  const WSL = require('is-wsl')
10
+ const debounce = require('lodash/debounce')
8
11
  const { default: omit } = require('omit.js')
12
+ const pEvent = require('p-event')
9
13
 
10
14
  const { name, version } = require('../../package.json')
11
15
  const { clearSpinner, startSpinner } = require('../lib/spinner')
@@ -198,6 +202,34 @@ const normalizeConfig = (config) =>
198
202
  ? { ...config, build: omit(config.build, ['publish', 'publishOrigin']) }
199
203
  : config
200
204
 
205
+ const DEBOUNCE_WAIT = 100
206
+
207
+ const watchDebounced = async (target, { depth, onAdd = () => {}, onChange = () => {}, onUnlink = () => {} }) => {
208
+ const watcher = chokidar.watch(target, { depth, ignored: /node_modules/, ignoreInitial: true })
209
+
210
+ await pEvent(watcher, 'ready')
211
+
212
+ const debouncedOnChange = debounce(onChange, DEBOUNCE_WAIT)
213
+ const debouncedOnUnlink = debounce(onUnlink, DEBOUNCE_WAIT)
214
+ const debouncedOnAdd = debounce(onAdd, DEBOUNCE_WAIT)
215
+
216
+ watcher
217
+ .on('change', (path) => {
218
+ decache(path)
219
+ debouncedOnChange(path)
220
+ })
221
+ .on('unlink', (path) => {
222
+ decache(path)
223
+ debouncedOnUnlink(path)
224
+ })
225
+ .on('add', (path) => {
226
+ decache(path)
227
+ debouncedOnAdd(path)
228
+ })
229
+
230
+ return watcher
231
+ }
232
+
201
233
  module.exports = {
202
234
  BANG,
203
235
  chalk,
@@ -217,4 +249,5 @@ module.exports = {
217
249
  sortOptions,
218
250
  USER_AGENT,
219
251
  warn,
252
+ watchDebounced,
220
253
  }
@@ -1,35 +0,0 @@
1
- // @ts-check
2
- const chokidar = require('chokidar')
3
- const decache = require('decache')
4
- const debounce = require('lodash/debounce')
5
- const pEvent = require('p-event')
6
-
7
- const DEBOUNCE_WAIT = 100
8
-
9
- const watchDebounced = async (target, { depth, onAdd = () => {}, onChange = () => {}, onUnlink = () => {} }) => {
10
- const watcher = chokidar.watch(target, { depth, ignored: /node_modules/, ignoreInitial: true })
11
-
12
- await pEvent(watcher, 'ready')
13
-
14
- const debouncedOnChange = debounce(onChange, DEBOUNCE_WAIT)
15
- const debouncedOnUnlink = debounce(onUnlink, DEBOUNCE_WAIT)
16
- const debouncedOnAdd = debounce(onAdd, DEBOUNCE_WAIT)
17
-
18
- watcher
19
- .on('change', (path) => {
20
- decache(path)
21
- debouncedOnChange(path)
22
- })
23
- .on('unlink', (path) => {
24
- decache(path)
25
- debouncedOnUnlink(path)
26
- })
27
- .on('add', (path) => {
28
- decache(path)
29
- debouncedOnAdd(path)
30
- })
31
-
32
- return watcher
33
- }
34
-
35
- module.exports = { watchDebounced }