netlify-cli 9.4.4 → 9.5.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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "9.4.4",
3
+ "version": "9.5.0",
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.5.0",
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.5.0",
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",
@@ -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')
@@ -78,6 +79,30 @@ const isNonExistingCommandError = ({ command, error: commandError }) => {
78
79
  )
79
80
  }
80
81
 
82
+ /**
83
+ * @type {(() => Promise<void>)[]} - array of functions to run before the process exits
84
+ */
85
+ const cleanupWork = []
86
+
87
+ let cleanupStarted = false
88
+
89
+ /**
90
+ * @param {object} input
91
+ * @param {number=} input.exitCode The exit code to return when exiting the process after cleanup
92
+ */
93
+ const cleanupBeforeExit = async ({ exitCode }) => {
94
+ // If cleanup has started, then wherever started it will be responsible for exiting
95
+ if (!cleanupStarted) {
96
+ cleanupStarted = true
97
+ try {
98
+ // eslint-disable-next-line no-unused-vars
99
+ const cleanupFinished = await Promise.all(cleanupWork.map((cleanup) => cleanup()))
100
+ } finally {
101
+ process.exit(exitCode)
102
+ }
103
+ }
104
+ }
105
+
81
106
  /**
82
107
  * Run a command and pipe stdout, stderr and stdin
83
108
  * @param {string} command
@@ -100,30 +125,29 @@ const runCommand = (command, env = {}) => {
100
125
 
101
126
  // we can't try->await->catch since we don't want to block on the framework server which
102
127
  // 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()
128
+ // eslint-disable-next-line promise/catch-or-return
129
+ commandProcess
130
+ // eslint-disable-next-line promise/prefer-await-to-then
131
+ .then(async () => {
132
+ const result = await commandProcess
133
+ const [commandWithoutArgs] = command.split(' ')
134
+ if (result.failed && isNonExistingCommandError({ command: commandWithoutArgs, error: result })) {
135
+ log(
136
+ NETLIFYDEVERR,
137
+ `Failed running command: ${command}. Please verify ${chalk.magenta(`'${commandWithoutArgs}'`)} exists`,
138
+ )
139
+ } else {
140
+ const errorMessage = result.failed
141
+ ? `${NETLIFYDEVERR} ${result.shortMessage}`
142
+ : `${NETLIFYDEVWARN} "${command}" exited with code ${result.exitCode}`
143
+
144
+ log(`${errorMessage}. Shutting down Netlify Dev server`)
145
+ }
146
+
147
+ return await cleanupBeforeExit({ exitCode: 1 })
126
148
  })
149
+ ;['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP', 'exit'].forEach((signal) => {
150
+ process.on(signal, async () => await cleanupBeforeExit({}))
127
151
  })
128
152
 
129
153
  return commandProcess
@@ -336,12 +360,18 @@ const dev = async (options, command) => {
336
360
  const oneGraphSessionId = loadCLISession(state)
337
361
 
338
362
  await persistNewOperationsDocForSession({
363
+ netlifyGraphConfig,
339
364
  netlifyToken,
340
365
  oneGraphSessionId,
341
366
  operationsDoc: graphqlDocument,
342
367
  siteId: site.id,
368
+ siteRoot: site.root,
343
369
  })
344
370
 
371
+ const cleanupSession = () => markCliSessionInactive({ netlifyToken, sessionId: oneGraphSessionId, siteId: site.id })
372
+
373
+ cleanupWork.push(cleanupSession)
374
+
345
375
  const graphEditUrl = getGraphEditUrlBySiteId({ siteId: site.id, oneGraphSessionId })
346
376
 
347
377
  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
43
+ if (!userOperationName) {
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
  )
@@ -24,7 +24,13 @@ const {
24
24
 
25
25
  const { parse } = GraphQL
26
26
  const { defaultExampleOperationsDoc, extractFunctionsFromOperationDoc } = NetlifyGraph
27
- const { createPersistedQuery, ensureAppForSite, updateCLISessionMetadata } = OneGraphClient
27
+ const {
28
+ createPersistedQuery,
29
+ ensureAppForSite,
30
+ executeMarkCliSessionActiveHeartbeat,
31
+ executeMarkCliSessionInactive,
32
+ updateCLISessionMetadata,
33
+ } = OneGraphClient
28
34
 
29
35
  const internalConsole = {
30
36
  log,
@@ -51,13 +57,31 @@ InternalConsole.registerConsole(internalConsole)
51
57
  * @param {function} input.onEvents A function to call when CLI events are received and need to be processed
52
58
  * @param {string} input.sessionId The session id to monitor CLI events for
53
59
  * @param {StateConfig} input.state A function to call to set/get the current state of the local Netlify project
60
+ * @param {any} input.site The site object
54
61
  * @returns
55
62
  */
56
63
  const monitorCLISessionEvents = (input) => {
57
- const { appId, netlifyGraphConfig, netlifyToken, onClose, onError, onEvents, sessionId, state } = input
64
+ const { appId, netlifyGraphConfig, netlifyToken, onClose, onError, onEvents, sessionId, site, state } = input
58
65
 
59
66
  const frequency = 5000
67
+ // 30 minutes
68
+ const defaultHeartbeatFrequency = 1_800_000
60
69
  let shouldClose = false
70
+ let nextMarkActiveHeartbeat = defaultHeartbeatFrequency
71
+
72
+ const markActiveHelper = async () => {
73
+ const fullSession = await OneGraphClient.fetchCliSession({ authToken: netlifyToken, appId, sessionId })
74
+ // @ts-ignore
75
+ const heartbeatIntervalms = fullSession.session.cliHeartbeatIntervalMs || defaultHeartbeatFrequency
76
+ nextMarkActiveHeartbeat = heartbeatIntervalms
77
+ const markCLISessionActiveResult = await executeMarkCliSessionActiveHeartbeat(netlifyToken, site.id, sessionId)
78
+ if (markCLISessionActiveResult.errors && markCLISessionActiveResult.errors.length !== 0) {
79
+ warn(`Failed to mark CLI session active: ${markCLISessionActiveResult.errors.join(', ')}`)
80
+ }
81
+ setTimeout(markActiveHelper, nextMarkActiveHeartbeat)
82
+ }
83
+
84
+ setTimeout(markActiveHelper, nextMarkActiveHeartbeat)
61
85
 
62
86
  const enabledServiceWatcher = async (innerNetlifyToken, siteId) => {
63
87
  const enabledServices = state.get('oneGraphEnabledServices') || ['onegraph']
@@ -371,6 +395,7 @@ const upsertMergeCLISessionMetadata = async ({ netlifyToken, newMetadata, oneGra
371
395
 
372
396
  const detectedMetadata = detectLocalCLISessionMetadata({ siteRoot })
373
397
 
398
+ // @ts-ignore
374
399
  const finalMetadata = { ...metadata, ...detectedMetadata, ...newMetadata }
375
400
  return OneGraphClient.updateCLISessionMetadata(netlifyToken, siteId, oneGraphSessionId, finalMetadata)
376
401
  }
@@ -429,19 +454,13 @@ const loadCLISession = (state) => state.get('oneGraphSessionId')
429
454
  const startOneGraphCLISession = async (input) => {
430
455
  const { netlifyGraphConfig, netlifyToken, site, state } = input
431
456
  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
- }
457
+
458
+ const oneGraphSessionId = await ensureCLISession({
459
+ metadata: {},
460
+ netlifyToken,
461
+ site,
462
+ state,
463
+ })
445
464
 
446
465
  const enabledServices = []
447
466
  const schema = await OneGraphClient.fetchOneGraphSchema(site.id, enabledServices)
@@ -481,6 +500,7 @@ const startOneGraphCLISession = async (input) => {
481
500
  netlifyToken,
482
501
  netlifyGraphConfig,
483
502
  sessionId: oneGraphSessionId,
503
+ site,
484
504
  state,
485
505
  onEvents: async (events) => {
486
506
  for (const event of events) {
@@ -500,6 +520,20 @@ const startOneGraphCLISession = async (input) => {
500
520
  })
501
521
  }
502
522
 
523
+ /**
524
+ * Mark a session as inactive so it doesn't show up in any UI lists, and potentially becomes available to GC later
525
+ * @param {object} input
526
+ * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
527
+ * @param {string} input.siteId A function to call to set/get the current state of the local Netlify project
528
+ * @param {string} input.sessionId The session id to monitor CLI events for
529
+ */
530
+ const markCliSessionInactive = async ({ netlifyToken, sessionId, siteId }) => {
531
+ const result = await executeMarkCliSessionInactive(netlifyToken, siteId, sessionId)
532
+ if (result.errors) {
533
+ warn(`Unable to mark CLI session ${sessionId} inactive: ${JSON.stringify(result.errors, null, 2)}`)
534
+ }
535
+ }
536
+
503
537
  /**
504
538
  * Generate a session name that can be identified as belonging to the current checkout
505
539
  * @returns {string} The name of the session to create
@@ -511,6 +545,70 @@ const generateSessionName = () => {
511
545
  return sessionName
512
546
  }
513
547
 
548
+ /**
549
+ * Ensures a cli session exists for the current checkout, or errors out if it doesn't and cannot create one.
550
+ */
551
+ const ensureCLISession = async ({ metadata, netlifyToken, site, state }) => {
552
+ let oneGraphSessionId = loadCLISession(state)
553
+ let parentCliSessionId = null
554
+
555
+ // Validate that session still exists and we can access it
556
+ try {
557
+ if (oneGraphSessionId) {
558
+ const sessionEvents = await OneGraphClient.fetchCliSessionEvents({
559
+ appId: site.id,
560
+ authToken: netlifyToken,
561
+ sessionId: oneGraphSessionId,
562
+ })
563
+ if (sessionEvents.errors) {
564
+ warn(`Unable to fetch cli session: ${JSON.stringify(sessionEvents.errors, null, 2)}`)
565
+ log(`Creating new cli session`)
566
+ parentCliSessionId = oneGraphSessionId
567
+ oneGraphSessionId = null
568
+ }
569
+ }
570
+ } catch (fetchSessionError) {
571
+ warn(`Unable to fetch cli session events: ${JSON.stringify(fetchSessionError, null, 2)}`)
572
+ oneGraphSessionId = null
573
+ }
574
+
575
+ if (!oneGraphSessionId) {
576
+ // If we can't access the session in the state.json or it doesn't exist, create a new one
577
+ const sessionName = generateSessionName()
578
+ const detectedMetadata = detectLocalCLISessionMetadata({ siteRoot: site.root })
579
+ const newSessionMetadata = parentCliSessionId ? { parentCliSessionId } : {}
580
+ const sessionMetadata = {
581
+ ...detectedMetadata,
582
+ ...newSessionMetadata,
583
+ ...metadata,
584
+ }
585
+ const oneGraphSession = await createCLISession({
586
+ netlifyToken,
587
+ siteId: site.id,
588
+ sessionName,
589
+ metadata: sessionMetadata,
590
+ })
591
+ state.set('oneGraphSessionId', oneGraphSession.id)
592
+ oneGraphSessionId = state.get('oneGraphSessionId')
593
+ }
594
+
595
+ if (!oneGraphSessionId) {
596
+ error('Unable to create or access Netlify Graph CLI session')
597
+ }
598
+
599
+ const { errors: markCLISessionActiveErrors } = await executeMarkCliSessionActiveHeartbeat(
600
+ netlifyToken,
601
+ site.id,
602
+ oneGraphSessionId,
603
+ )
604
+
605
+ if (markCLISessionActiveErrors) {
606
+ warn(`Unable to mark cli session active: ${JSON.stringify(markCLISessionActiveErrors, null, 2)}`)
607
+ }
608
+
609
+ return oneGraphSessionId
610
+ }
611
+
514
612
  const OneGraphCliClient = {
515
613
  ackCLISessionEvents: OneGraphClient.ackCLISessionEvents,
516
614
  createPersistedQuery,
@@ -522,10 +620,12 @@ const OneGraphCliClient = {
522
620
  module.exports = {
523
621
  OneGraphCliClient,
524
622
  createCLISession,
623
+ ensureCLISession,
525
624
  extractFunctionsFromOperationDoc,
526
625
  handleCliSessionEvent,
527
626
  generateSessionName,
528
627
  loadCLISession,
628
+ markCliSessionInactive,
529
629
  monitorCLISessionEvents,
530
630
  persistNewOperationsDocForSession,
531
631
  refetchAndGenerateFromOneGraph,