netlify-cli 8.16.0 → 8.17.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "8.16.0",
4
+ "version": "8.17.2",
5
5
  "author": "Netlify Inc.",
6
6
  "contributors": [
7
7
  "Mathias Biilmann <matt@netlify.com> (https://twitter.com/biilmann)",
@@ -63,7 +63,7 @@
63
63
  "test:dev:ava": "ava --verbose",
64
64
  "test:ci:ava:unit": "c8 -r json ava --no-worker-threads tests/unit/**/*.test.js tools/**/*.test.js",
65
65
  "test:ci:ava:integration": "c8 -r json ava --concurrency 1 --no-worker-threads tests/integration/**/*.test.js",
66
- "test:affected": "node ./tools/affected-test.js",
66
+ "test:affected": "node ./tools/affected-test.mjs",
67
67
  "e2e": "node ./tools/e2e/run.mjs",
68
68
  "docs": "node ./site/scripts/docs.mjs",
69
69
  "watch": "c8 --reporter=lcov ava --watch",
@@ -73,7 +73,7 @@
73
73
  "postinstall": "node ./scripts/postinstall.js"
74
74
  },
75
75
  "config": {
76
- "eslint": "--ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,tools,scripts,site,tests,.github}/**/*.{mjs,cjs,js,md,html}\" \"*.{mjs,cjs,js,md,html}\" \".*.{mjs,cjs,js,md,html}\"",
76
+ "eslint": "--ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,scripts,site,tests,.github}/**/*.{mjs,cjs,js,md,html}\" \"*.{mjs,cjs,js,md,html}\" \".*.{mjs,cjs,js,md,html}\"",
77
77
  "prettier": "--ignore-path .gitignore --loglevel=warn \"{src,tools,scripts,site,tests,.github}/**/*.{mjs,cjs,js,md,yml,json,html}\" \"*.{mjs,cjs,js,yml,json,html}\" \".*.{mjs,cjs,js,yml,json,html}\" \"!CHANGELOG.md\" \"!npm-shrinkwrap.json\" \"!.github/**/*.md\""
78
78
  },
79
79
  "dependencies": {
@@ -98,7 +98,7 @@
98
98
  "chokidar": "^3.0.2",
99
99
  "ci-info": "^3.0.0",
100
100
  "clean-deep": "^3.0.2",
101
- "commander": "^8.3.0",
101
+ "commander": "^9.0.0",
102
102
  "concordance": "^5.0.0",
103
103
  "configstore": "^5.0.0",
104
104
  "content-type": "^1.0.4",
@@ -184,11 +184,11 @@
184
184
  "uuid": "^8.0.0",
185
185
  "wait-port": "^0.2.2",
186
186
  "winston": "^3.2.1",
187
- "write-file-atomic": "^3.0.0"
187
+ "write-file-atomic": "^4.0.0"
188
188
  },
189
189
  "devDependencies": {
190
190
  "@babel/preset-react": "^7.12.13",
191
- "@netlify/eslint-config-node": "^4.1.7",
191
+ "@netlify/eslint-config-node": "^5.1.2",
192
192
  "ava": "^4.0.0",
193
193
  "c8": "^7.11.0",
194
194
  "eslint-plugin-sort-destructure-keys": "^1.3.5",
@@ -200,7 +200,7 @@
200
200
  "ini": "^2.0.0",
201
201
  "mock-fs": "^5.1.2",
202
202
  "p-timeout": "^4.0.0",
203
- "proxyquire": "^2.1.3",
203
+ "rewiremock": "^3.14.3",
204
204
  "seedrandom": "^3.0.5",
205
205
  "serialize-javascript": "^6.0.0",
206
206
  "sinon": "^13.0.0",
@@ -215,7 +215,7 @@
215
215
  },
216
216
  "ava": {
217
217
  "files": [
218
- "tools/**/*.test.js",
218
+ "tools/**/*.test.mjs",
219
219
  "tests/**/*.test.js"
220
220
  ],
221
221
  "cache": true,
@@ -11,8 +11,18 @@ const stripAnsiCc = require('strip-ansi-control-characters')
11
11
  const waitPort = require('wait-port')
12
12
 
13
13
  const { startFunctionsServer } = require('../../lib/functions/server')
14
- const { OneGraphCliClient, startOneGraphCLISession } = require('../../lib/one-graph/cli-client')
15
- const { getNetlifyGraphConfig } = require('../../lib/one-graph/cli-netlify-graph')
14
+ const {
15
+ OneGraphCliClient,
16
+ loadCLISession,
17
+ persistNewOperationsDocForSession,
18
+ startOneGraphCLISession,
19
+ } = require('../../lib/one-graph/cli-client')
20
+ const {
21
+ defaultExampleOperationsDoc,
22
+ getGraphEditUrlBySiteId,
23
+ getNetlifyGraphConfig,
24
+ readGraphQLOperationsSourceFile,
25
+ } = require('../../lib/one-graph/cli-netlify-graph')
16
26
  const {
17
27
  NETLIFYDEV,
18
28
  NETLIFYDEVERR,
@@ -349,9 +359,29 @@ const dev = async (options, command) => {
349
359
  await OneGraphCliClient.ensureAppForSite(netlifyToken, site.id)
350
360
  const netlifyGraphConfig = await getNetlifyGraphConfig({ command, options, settings })
351
361
 
352
- log(`Starting Netlify Graph session, to edit your library run \`netlify graph:edit\` in another tab`)
362
+ let graphqlDocument = readGraphQLOperationsSourceFile(netlifyGraphConfig)
363
+
364
+ if (!graphqlDocument || graphqlDocument.trim().length === 0) {
365
+ graphqlDocument = defaultExampleOperationsDoc
366
+ }
367
+
368
+ await startOneGraphCLISession({ netlifyGraphConfig, netlifyToken, site, state })
369
+
370
+ // Should be created by startOneGraphCLISession
371
+ const oneGraphSessionId = loadCLISession(state)
353
372
 
354
- startOneGraphCLISession({ netlifyGraphConfig, netlifyToken, site, state })
373
+ await persistNewOperationsDocForSession({
374
+ netlifyToken,
375
+ oneGraphSessionId,
376
+ operationsDoc: graphqlDocument,
377
+ siteId: site.id,
378
+ })
379
+
380
+ const graphEditUrl = getGraphEditUrlBySiteId({ siteId: site.id, oneGraphSessionId })
381
+
382
+ log(
383
+ `Starting Netlify Graph session, to edit your library visit ${graphEditUrl} or run \`netlify graph:edit\` in another tab`,
384
+ )
355
385
  }
356
386
 
357
387
  printBanner({ url })
@@ -3,7 +3,7 @@ const gitRepoInfo = require('git-repo-info')
3
3
  const { OneGraphCliClient, generateSessionName, loadCLISession } = require('../../lib/one-graph/cli-client')
4
4
  const {
5
5
  defaultExampleOperationsDoc,
6
- getGraphEditUrlBySiteName,
6
+ getGraphEditUrlBySiteId,
7
7
  getNetlifyGraphConfig,
8
8
  readGraphQLOperationsSourceFile,
9
9
  } = require('../../lib/one-graph/cli-netlify-graph')
@@ -19,7 +19,7 @@ const { createCLISession, createPersistedQuery, ensureAppForSite, updateCLISessi
19
19
  * @returns
20
20
  */
21
21
  const graphEdit = async (options, command) => {
22
- const { api, site, siteInfo, state } = command.netlify
22
+ const { site, state } = command.netlify
23
23
  const siteId = site.id
24
24
 
25
25
  if (!site.id) {
@@ -60,17 +60,7 @@ const graphEdit = async (options, command) => {
60
60
 
61
61
  await updateCLISessionMetadata(netlifyToken, siteId, oneGraphSessionId, { docId: persistedDoc.id })
62
62
 
63
- let siteName = siteInfo.name
64
-
65
- if (!siteName) {
66
- const siteData = await api.getSite({ siteId })
67
- siteName = siteData.name
68
- if (!siteName) {
69
- error(`No site name found for siteId ${siteId}`)
70
- }
71
- }
72
-
73
- const graphEditUrl = getGraphEditUrlBySiteName({ siteName, oneGraphSessionId })
63
+ const graphEditUrl = getGraphEditUrlBySiteId({ siteId, oneGraphSessionId })
74
64
 
75
65
  await openBrowser({ url: graphEditUrl })
76
66
  }
@@ -9,7 +9,7 @@
9
9
  "version": "1.0.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "stripe": "^8.201.0"
12
+ "stripe": "^8.202.0"
13
13
  }
14
14
  },
15
15
  "node_modules/@types/node": {
@@ -105,9 +105,9 @@
105
105
  }
106
106
  },
107
107
  "node_modules/stripe": {
108
- "version": "8.201.0",
109
- "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.201.0.tgz",
110
- "integrity": "sha512-pF0F1DdE9zt0U6Cb0XN+REpdFkUmaqp6C7OEVOCeUpTAafjjJqrdV/WmZd7Y5MwT8XvDAxB5/v3CAXwxAp0XNg==",
108
+ "version": "8.202.0",
109
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.202.0.tgz",
110
+ "integrity": "sha512-3YGHVnUatEn/At5+aRy+REdB2IyVa96/zls2xvQrKFTgaJzRu1MsJcK0GKg0p2B0y0VqlZo9gmdDEqphSHHvtA==",
111
111
  "dependencies": {
112
112
  "@types/node": ">=8.1.0",
113
113
  "qs": "^6.6.0"
@@ -184,9 +184,9 @@
184
184
  }
185
185
  },
186
186
  "stripe": {
187
- "version": "8.201.0",
188
- "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.201.0.tgz",
189
- "integrity": "sha512-pF0F1DdE9zt0U6Cb0XN+REpdFkUmaqp6C7OEVOCeUpTAafjjJqrdV/WmZd7Y5MwT8XvDAxB5/v3CAXwxAp0XNg==",
187
+ "version": "8.202.0",
188
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.202.0.tgz",
189
+ "integrity": "sha512-3YGHVnUatEn/At5+aRy+REdB2IyVa96/zls2xvQrKFTgaJzRu1MsJcK0GKg0p2B0y0VqlZo9gmdDEqphSHHvtA==",
190
190
  "requires": {
191
191
  "@types/node": ">=8.1.0",
192
192
  "qs": "^6.6.0"
@@ -9,7 +9,7 @@
9
9
  "version": "1.0.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "stripe": "^8.201.0"
12
+ "stripe": "^8.202.0"
13
13
  }
14
14
  },
15
15
  "node_modules/@types/node": {
@@ -105,9 +105,9 @@
105
105
  }
106
106
  },
107
107
  "node_modules/stripe": {
108
- "version": "8.201.0",
109
- "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.201.0.tgz",
110
- "integrity": "sha512-pF0F1DdE9zt0U6Cb0XN+REpdFkUmaqp6C7OEVOCeUpTAafjjJqrdV/WmZd7Y5MwT8XvDAxB5/v3CAXwxAp0XNg==",
108
+ "version": "8.202.0",
109
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.202.0.tgz",
110
+ "integrity": "sha512-3YGHVnUatEn/At5+aRy+REdB2IyVa96/zls2xvQrKFTgaJzRu1MsJcK0GKg0p2B0y0VqlZo9gmdDEqphSHHvtA==",
111
111
  "dependencies": {
112
112
  "@types/node": ">=8.1.0",
113
113
  "qs": "^6.6.0"
@@ -184,9 +184,9 @@
184
184
  }
185
185
  },
186
186
  "stripe": {
187
- "version": "8.201.0",
188
- "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.201.0.tgz",
189
- "integrity": "sha512-pF0F1DdE9zt0U6Cb0XN+REpdFkUmaqp6C7OEVOCeUpTAafjjJqrdV/WmZd7Y5MwT8XvDAxB5/v3CAXwxAp0XNg==",
187
+ "version": "8.202.0",
188
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.202.0.tgz",
189
+ "integrity": "sha512-3YGHVnUatEn/At5+aRy+REdB2IyVa96/zls2xvQrKFTgaJzRu1MsJcK0GKg0p2B0y0VqlZo9gmdDEqphSHHvtA==",
190
190
  "requires": {
191
191
  "@types/node": ">=8.1.0",
192
192
  "qs": "^6.6.0"
@@ -10,8 +10,8 @@
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@netlify/functions": "^0.11.0",
13
- "@types/node": "^14.0.0",
14
- "typescript": "^4.5.5"
13
+ "@types/node": "^14.18.10",
14
+ "typescript": "^4.0.0"
15
15
  }
16
16
  },
17
17
  "node_modules/@netlify/functions": {
@@ -26,9 +26,9 @@
26
26
  }
27
27
  },
28
28
  "node_modules/@types/node": {
29
- "version": "14.18.9",
30
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.9.tgz",
31
- "integrity": "sha512-j11XSuRuAlft6vLDEX4RvhqC0KxNxx6QIyMXNb0vHHSNPXTPeiy3algESWmOOIzEtiEL0qiowPU3ewW9hHVa7Q=="
29
+ "version": "14.18.10",
30
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.10.tgz",
31
+ "integrity": "sha512-6iihJ/Pp5fsFJ/aEDGyvT4pHGmCpq7ToQ/yf4bl5SbVAvwpspYJ+v3jO7n8UyjhQVHTy+KNszOozDdv+O6sovQ=="
32
32
  },
33
33
  "node_modules/is-promise": {
34
34
  "version": "4.0.0",
@@ -58,9 +58,9 @@
58
58
  }
59
59
  },
60
60
  "@types/node": {
61
- "version": "14.18.9",
62
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.9.tgz",
63
- "integrity": "sha512-j11XSuRuAlft6vLDEX4RvhqC0KxNxx6QIyMXNb0vHHSNPXTPeiy3algESWmOOIzEtiEL0qiowPU3ewW9hHVa7Q=="
61
+ "version": "14.18.10",
62
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.10.tgz",
63
+ "integrity": "sha512-6iihJ/Pp5fsFJ/aEDGyvT4pHGmCpq7ToQ/yf4bl5SbVAvwpspYJ+v3jO7n8UyjhQVHTy+KNszOozDdv+O6sovQ=="
64
64
  },
65
65
  "is-promise": {
66
66
  "version": "4.0.0",
@@ -8,7 +8,7 @@ const terminalLink = require('terminal-link')
8
8
 
9
9
  // cannot directly import from ../utils as it would create a circular dependency.
10
10
  // the file `src/utils/live-tunnel.js` depends on this file
11
- const { NETLIFYDEVWARN, chalk, error, log } = require('../utils/command-helpers')
11
+ const { NETLIFYDEVWARN, error, log } = require('../utils/command-helpers')
12
12
  const execa = require('../utils/execa')
13
13
 
14
14
  const isWindows = () => process.platform === 'win32'
@@ -127,9 +127,7 @@ const fetchLatestVersion = async ({ destination, execName, extension, latestVers
127
127
 
128
128
  const issueLink = terminalLink('Create a new CLI issue', createIssueLink.href)
129
129
 
130
- error(`The operating system ${chalk.cyan(platform)} with the CPU architecture ${chalk.cyan(
131
- arch,
132
- )} is currently not supported!
130
+ error(`The operating system ${platform} with the CPU architecture ${arch} is currently not supported!
133
131
 
134
132
  Please open up an issue on our CLI repository so that we can support it:
135
133
  ${issueLink}`)
@@ -1,13 +1,15 @@
1
1
  /* eslint-disable eslint-comments/disable-enable-pair */
2
2
  /* eslint-disable fp/no-loops */
3
+ const crypto = require('crypto')
3
4
  const os = require('os')
5
+ const path = require('path')
4
6
 
7
+ const gitRepoInfo = require('git-repo-info')
5
8
  const { GraphQL, InternalConsole, OneGraphClient } = require('netlify-onegraph-internal')
6
9
  const { NetlifyGraph } = require('netlify-onegraph-internal')
7
10
 
8
11
  const { chalk, error, log, warn } = require('../../utils')
9
-
10
- const { createCLISession, createPersistedQuery, ensureAppForSite, updateCLISessionMetadata } = OneGraphClient
12
+ const { watchDebounced } = require('../functions/watcher')
11
13
 
12
14
  const {
13
15
  generateFunctionsFile,
@@ -19,6 +21,7 @@ const {
19
21
 
20
22
  const { parse } = GraphQL
21
23
  const { defaultExampleOperationsDoc, extractFunctionsFromOperationDoc } = NetlifyGraph
24
+ const { createCLISession, createPersistedQuery, ensureAppForSite, updateCLISessionMetadata } = OneGraphClient
22
25
 
23
26
  const internalConsole = {
24
27
  log,
@@ -27,6 +30,9 @@ const internalConsole = {
27
30
  debug: console.debug,
28
31
  }
29
32
 
33
+ const witnessedIncomingDocumentHashes = []
34
+
35
+ // Keep track of which document hashes we've received from the server so we can ignore events from the filesystem based on them
30
36
  InternalConsole.registerConsole(internalConsole)
31
37
 
32
38
  /**
@@ -108,6 +114,26 @@ const monitorCLISessionEvents = (input) => {
108
114
  return close
109
115
  }
110
116
 
117
+ /**
118
+ * Monitor the operations document for changes
119
+ * @param {object} input
120
+ * @param {NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
121
+ * @param {function} input.onAdd A callback function to handle when the operations document is added
122
+ * @param {function} input.onChange A callback function to handle when the operations document is changed
123
+ * @param {function} input.onUnlink A callback function to handle when the operations document is unlinked
124
+ * @returns {Promise<watcher>}
125
+ */
126
+ const monitorOperationFile = async ({ netlifyGraphConfig, onAdd, onChange, onUnlink }) => {
127
+ const filePath = path.resolve(...netlifyGraphConfig.graphQLOperationsSourceFilename)
128
+ const newWatcher = await watchDebounced([filePath], {
129
+ onAdd,
130
+ onChange,
131
+ onUnlink,
132
+ })
133
+
134
+ return newWatcher
135
+ }
136
+
111
137
  /**
112
138
  * Fetch the schema for a site, and regenerate all of the downstream files
113
139
  * @param {object} input
@@ -146,7 +172,44 @@ const refetchAndGenerateFromOneGraph = async (input) => {
146
172
  }
147
173
 
148
174
  /**
149
- *
175
+ * Regenerate the function library based on the current operations document on disk
176
+ * @param {object} input
177
+ * @param {string} input.schema The GraphQL schema to use when generating code
178
+ * @param {NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
179
+ * @returns
180
+ */
181
+ const regenerateFunctionsFileFromOperationsFile = (input) => {
182
+ const { netlifyGraphConfig, schema } = input
183
+
184
+ const appOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
185
+
186
+ const hash = quickHash(appOperationsDoc)
187
+
188
+ if (witnessedIncomingDocumentHashes.includes(hash)) {
189
+ // We've already seen this document, so don't regenerate
190
+ return
191
+ }
192
+
193
+ const parsedDoc = parse(appOperationsDoc, {
194
+ noLocation: true,
195
+ })
196
+ const { fragments, functions } = extractFunctionsFromOperationDoc(parsedDoc)
197
+ generateFunctionsFile({ netlifyGraphConfig, schema, operationsDoc: appOperationsDoc, functions, fragments })
198
+ }
199
+
200
+ /**
201
+ * Compute a md5 hash of a string
202
+ * @param {string} input String to compute a quick md5 hash for
203
+ * @returns hex digest of the input string
204
+ */
205
+ const quickHash = (input) => {
206
+ const hashSum = crypto.createHash('md5')
207
+ hashSum.update(input)
208
+ return hashSum.digest('hex')
209
+ }
210
+
211
+ /**
212
+ * Fetch a persisted operations doc by its id, write it to the system, and regenerate the library
150
213
  * @param {object} input
151
214
  * @param {string} input.siteId The site id to query against
152
215
  * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
@@ -155,7 +218,7 @@ const refetchAndGenerateFromOneGraph = async (input) => {
155
218
  * @param {NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
156
219
  * @returns
157
220
  */
158
- const updateGraphQLOperationsFile = async (input) => {
221
+ const updateGraphQLOperationsFileFromPersistedDoc = async (input) => {
159
222
  const { docId, netlifyGraphConfig, netlifyToken, schema, siteId } = input
160
223
  const persistedDoc = await OneGraphClient.fetchPersistedQuery(netlifyToken, siteId, docId)
161
224
  if (!persistedDoc) {
@@ -166,12 +229,17 @@ const updateGraphQLOperationsFile = async (input) => {
166
229
  const doc = persistedDoc.query
167
230
 
168
231
  writeGraphQLOperationsSourceFile(netlifyGraphConfig, doc)
169
- const appOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
170
- const parsedDoc = parse(appOperationsDoc, {
171
- noLocation: true,
172
- })
173
- const { fragments, functions } = extractFunctionsFromOperationDoc(parsedDoc)
174
- generateFunctionsFile({ netlifyGraphConfig, schema, operationsDoc: appOperationsDoc, functions, fragments })
232
+ regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })
233
+
234
+ const hash = quickHash(doc)
235
+
236
+ const relevantHasLength = 10
237
+
238
+ if (witnessedIncomingDocumentHashes.length > relevantHasLength) {
239
+ witnessedIncomingDocumentHashes.shift()
240
+ }
241
+
242
+ witnessedIncomingDocumentHashes.push(hash)
175
243
  }
176
244
 
177
245
  const handleCliSessionEvent = async ({ event, netlifyGraphConfig, netlifyToken, schema, siteId }) => {
@@ -184,7 +252,13 @@ const handleCliSessionEvent = async ({ event, netlifyGraphConfig, netlifyToken,
184
252
  await generateHandler(netlifyGraphConfig, schema, payload.operationId, payload)
185
253
  break
186
254
  case 'OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent':
187
- await updateGraphQLOperationsFile({ netlifyToken, docId: payload.docId, netlifyGraphConfig, schema, siteId })
255
+ await updateGraphQLOperationsFileFromPersistedDoc({
256
+ netlifyToken,
257
+ docId: payload.docId,
258
+ netlifyGraphConfig,
259
+ schema,
260
+ siteId,
261
+ })
188
262
  break
189
263
  default: {
190
264
  warn(`Unrecognized event received, you may need to upgrade your CLI version`, __typename, payload)
@@ -193,6 +267,24 @@ const handleCliSessionEvent = async ({ event, netlifyGraphConfig, netlifyToken,
193
267
  }
194
268
  }
195
269
 
270
+ const persistNewOperationsDocForSession = async ({ netlifyToken, oneGraphSessionId, operationsDoc, siteId }) => {
271
+ const { branch } = gitRepoInfo()
272
+
273
+ const payload = {
274
+ appId: siteId,
275
+ description: 'Temporary snapshot of local queries',
276
+ document: operationsDoc,
277
+ tags: ['netlify-cli', `session:${oneGraphSessionId}`, `git-branch:${branch}`, `local-change`],
278
+ }
279
+ const persistedDoc = await createPersistedQuery(netlifyToken, payload)
280
+ const newMetadata = await { docId: persistedDoc.id }
281
+ const result = await OneGraphClient.updateCLISessionMetadata(netlifyToken, siteId, oneGraphSessionId, newMetadata)
282
+
283
+ if (result.errors) {
284
+ warn('Unable to update session metadata with updated operations doc', result.errors)
285
+ }
286
+ }
287
+
196
288
  /**
197
289
  * Load the CLI session id from the local state
198
290
  * @param {state} state
@@ -201,7 +293,7 @@ const handleCliSessionEvent = async ({ event, netlifyGraphConfig, netlifyToken,
201
293
  const loadCLISession = (state) => state.get('oneGraphSessionId')
202
294
 
203
295
  /**
204
- * Idemponentially save the CLI session id to the local state and start monitoring for CLI events and upstream schema changes
296
+ * Idemponentially save the CLI session id to the local state and start monitoring for CLI events, upstream schema changes, and local operation file changes
205
297
  * @param {object} input
206
298
  * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
207
299
  * @param {NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
@@ -223,6 +315,32 @@ const startOneGraphCLISession = async (input) => {
223
315
  const enabledServices = []
224
316
  const schema = await OneGraphClient.fetchOneGraphSchema(site.id, enabledServices)
225
317
 
318
+ monitorOperationFile({
319
+ netlifyGraphConfig,
320
+ onChange: async (filePath) => {
321
+ log('NetlifyGraph operation file changed at', filePath, 'updating function library...')
322
+ regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })
323
+ const newOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
324
+ await persistNewOperationsDocForSession({
325
+ netlifyToken,
326
+ oneGraphSessionId,
327
+ operationsDoc: newOperationsDoc,
328
+ siteId: site.id,
329
+ })
330
+ },
331
+ onAdd: async (filePath) => {
332
+ log('NetlifyGraph operation file created at', filePath, 'creating function library...')
333
+ regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })
334
+ const newOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
335
+ await persistNewOperationsDocForSession({
336
+ netlifyToken,
337
+ oneGraphSessionId,
338
+ operationsDoc: newOperationsDoc,
339
+ siteId: site.id,
340
+ })
341
+ },
342
+ })
343
+
226
344
  monitorCLISessionEvents({
227
345
  appId: site.id,
228
346
  netlifyToken,
@@ -273,6 +391,7 @@ module.exports = {
273
391
  generateSessionName,
274
392
  loadCLISession,
275
393
  monitorCLISessionEvents,
394
+ persistNewOperationsDocForSession,
276
395
  refetchAndGenerateFromOneGraph,
277
396
  startOneGraphCLISession,
278
397
  }
@@ -4,7 +4,7 @@ const process = require('process')
4
4
 
5
5
  const { GraphQL, InternalConsole, NetlifyGraph } = require('netlify-onegraph-internal')
6
6
 
7
- const { detectServerSettings, error, getFunctionsDir, log, warn } = require('../../utils')
7
+ const { detectServerSettings, error, execa, getFunctionsDir, log, warn } = require('../../utils')
8
8
 
9
9
  const { printSchema } = GraphQL
10
10
 
@@ -241,6 +241,31 @@ const ensureFunctionsPath = (netlifyGraphConfig) => {
241
241
  fs.mkdirSync(fullPath, { recursive: true })
242
242
  }
243
243
 
244
+ let disablePrettierDueToPreviousError = false
245
+
246
+ const runPrettier = async (filePath) => {
247
+ if (disablePrettierDueToPreviousError) {
248
+ return
249
+ }
250
+
251
+ const command = `prettier --write ${filePath}`
252
+ try {
253
+ const commandProcess = execa.command(command, {
254
+ preferLocal: true,
255
+ // windowsHide needs to be false for child process to terminate properly on Windows
256
+ windowsHide: false,
257
+ })
258
+
259
+ await commandProcess
260
+ } catch (prettierError) {
261
+ if (!disablePrettierDueToPreviousError) {
262
+ disablePrettierDueToPreviousError = true
263
+ warn(prettierError)
264
+ warn("Error while running prettier, make sure you have installed it globally with 'npm i -g prettier'")
265
+ }
266
+ }
267
+ }
268
+
244
269
  /**
245
270
  * Generate a library file with type definitions for a given NetlifyGraphConfig, operationsDoc, and schema, writing them to the filesystem
246
271
  * @param {object} context
@@ -267,6 +292,8 @@ const generateFunctionsFile = ({ fragments, functions, netlifyGraphConfig, opera
267
292
  typeDefinitionsSource,
268
293
  'utf8',
269
294
  )
295
+ runPrettier(path.resolve(...netlifyGraphConfig.netlifyGraphImplementationFilename))
296
+ runPrettier(path.resolve(...netlifyGraphConfig.netlifyGraphTypeDefinitionsFilename))
270
297
  }
271
298
 
272
299
  /**
@@ -384,6 +411,7 @@ const generateHandler = (netlifyGraphConfig, schema, operationId, handlerOptions
384
411
  const absoluteFilename = path.resolve(...filenameArr)
385
412
 
386
413
  fs.writeFileSync(absoluteFilename, content)
414
+ runPrettier(absoluteFilename)
387
415
  })
388
416
  }
389
417
 
@@ -405,6 +433,21 @@ const getGraphEditUrlBySiteName = ({ oneGraphSessionId, siteName }) => {
405
433
  return url
406
434
  }
407
435
 
436
+ /**
437
+ * Get a url to the Netlify Graph UI for the current session by a site's id
438
+ * @param {object} options
439
+ * @param {string} options.siteId The name of the site as used in the Netlify UI url scheme
440
+ * @param {string} options.oneGraphSessionId The oneGraph session id to use when generating the graph-edit link
441
+ * @returns {string} The url to the Netlify Graph UI for the current session
442
+ */
443
+ const getGraphEditUrlBySiteId = ({ oneGraphSessionId, siteId }) => {
444
+ const host = process.env.NETLIFY_APP_HOST || 'app.netlify.com'
445
+ // http because app.netlify.com will redirect to https, and localhost will still work for development
446
+ const url = `http://${host}/site-redirect/${siteId}/graph/explorer?cliSessionId=${oneGraphSessionId}`
447
+
448
+ return url
449
+ }
450
+
408
451
  module.exports = {
409
452
  buildSchema,
410
453
  defaultExampleOperationsDoc: NetlifyGraph.defaultExampleOperationsDoc,
@@ -413,6 +456,7 @@ module.exports = {
413
456
  generateFunctionsFile,
414
457
  generateHandlerSource: NetlifyGraph.generateHandlerSource,
415
458
  generateHandler,
459
+ getGraphEditUrlBySiteId,
416
460
  getGraphEditUrlBySiteName,
417
461
  getNetlifyGraphConfig,
418
462
  parse,
@@ -213,6 +213,10 @@ const serveRedirect = async function ({ match, options, proxy, req, res }) {
213
213
 
214
214
  const destURL = stripOrigin(dest)
215
215
 
216
+ if (isExternal(match)) {
217
+ return proxyToExternalUrl({ req, res, dest, destURL })
218
+ }
219
+
216
220
  if (isRedirect(match)) {
217
221
  res.writeHead(match.status, {
218
222
  Location: destURL,
@@ -222,10 +226,6 @@ const serveRedirect = async function ({ match, options, proxy, req, res }) {
222
226
  return
223
227
  }
224
228
 
225
- if (isExternal(match)) {
226
- return proxyToExternalUrl({ req, res, dest, destURL })
227
- }
228
-
229
229
  const ct = req.headers['content-type'] ? contentType.parse(req).type : ''
230
230
  if (
231
231
  req.method === 'POST' &&