netlify-cli 14.4.0 → 15.0.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,1033 +0,0 @@
1
- /* eslint-disable eslint-comments/disable-enable-pair */
2
-
3
- // @ts-check
4
- import fs from 'fs'
5
- import path from 'path'
6
- import process from 'process'
7
- import { pathToFileURL } from 'url'
8
-
9
- import inquirer from 'inquirer'
10
- import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt'
11
- import { GraphQL, GraphQLHelpers, IncludedCodegen, InternalConsole, NetlifyGraph } from 'netlify-onegraph-internal'
12
-
13
- import { chalk, error, log, warn } from '../../utils/command-helpers.mjs'
14
- import detectServerSettings from '../../utils/detect-server-settings.mjs'
15
- import execa from '../../utils/execa.mjs'
16
- import { getFunctionsDir } from '../../utils/functions/index.mjs'
17
-
18
- const { printSchema } = GraphQL
19
-
20
- const internalConsole = {
21
- log,
22
- warn,
23
- error,
24
- debug: console.debug,
25
- }
26
-
27
- InternalConsole.registerConsole(internalConsole)
28
-
29
- const { defaultExampleOperationsDoc, extractFunctionsFromOperationDoc, generateHandlerSource } = NetlifyGraph
30
- const { normalizeOperationsDoc } = GraphQLHelpers
31
-
32
- /**
33
- * Updates the netlify.toml in-place with the `graph.codeGenerator` key set to `codegenModuleImportPath
34
- * It's a very hacky, string-based implementation for because
35
- * 1. There isn't a good toml parser/updater/pretty-printer that preserves formatting and comments
36
- * 2. We want to make minimal changes to `netlify.toml`
37
- * @param {object} input
38
- * @param {string} input.siteRoot
39
- * @param {string} input.codegenModuleImportPath
40
- * @returns void
41
- */
42
- const setNetlifyTomlCodeGeneratorModule = ({ codegenModuleImportPath, siteRoot }) => {
43
- let toml
44
- let filepath
45
-
46
- try {
47
- const filepathArr = ['/', ...siteRoot.split(path.sep), 'netlify.toml']
48
- filepath = path.resolve(...filepathArr)
49
- const configText = fs.readFileSync(filepath, 'utf-8')
50
-
51
- toml = configText
52
- } catch (error_) {
53
- warn(`Error reading netlify.toml for Netlify Graph codegenModule update: ${error_}`)
54
- }
55
-
56
- if (!filepath) {
57
- return
58
- }
59
-
60
- const entry = ` codeGenerator = "${codegenModuleImportPath}"
61
- `
62
-
63
- const fullEntry = `
64
- [graph]
65
- ${entry}`
66
-
67
- if (!toml) {
68
- fs.writeFileSync(filepath, fullEntry, 'utf-8')
69
- return
70
- }
71
-
72
- const EOL = '\n'
73
- let lines = toml.split(EOL)
74
- const graphKeyLine = lines.findIndex((line) => line.trim().startsWith('[graph]'))
75
- const hasGraphKey = graphKeyLine !== -1
76
- const codegenKeyLine = lines.findIndex((line) => line.trim().startsWith('codeGenerator'))
77
- const hasCodegenKeyLine = codegenKeyLine !== 1
78
-
79
- if (hasGraphKey && hasCodegenKeyLine) {
80
- lines.splice(codegenKeyLine, 1, entry)
81
- } else if (hasGraphKey) {
82
- lines.splice(graphKeyLine, 0, entry)
83
- } else {
84
- lines = [...lines, ...fullEntry.split(EOL)]
85
- }
86
-
87
- const newToml = lines.join(EOL)
88
-
89
- fs.writeFileSync(filepath, newToml, 'utf-8')
90
- }
91
-
92
- /**
93
- * Remove any relative path components from the given path
94
- * @param {string[]} items Filesystem path items to filter
95
- * @return {string[]} Filtered filesystem path items
96
- */
97
- const filterRelativePathItems = (items) => items.filter((part) => part !== '')
98
-
99
- /**
100
- * Return the default Netlify Graph configuration for a generic site
101
- * @param {object} input
102
- * @param {object} input.baseConfig
103
- * @param {string[]} input.detectedFunctionsPath
104
- * @param {string[]} input.siteRoot
105
- */
106
- const makeDefaultNetlifyGraphConfig = ({ baseConfig, detectedFunctionsPath }) => {
107
- const functionsPath = filterRelativePathItems([...detectedFunctionsPath])
108
- const webhookBasePath = '/.netlify/functions'
109
- const netlifyGraphPath = [...functionsPath, 'netlifyGraph']
110
- const netlifyGraphImplementationFilename = [...netlifyGraphPath, `index.${baseConfig.extension}`]
111
- const netlifyGraphTypeDefinitionsFilename = [...netlifyGraphPath, `index.d.ts`]
112
- const graphQLOperationsSourceFilename = [...netlifyGraphPath, NetlifyGraph.defaultSourceOperationsFilename]
113
- const graphQLSchemaFilename = [...netlifyGraphPath, NetlifyGraph.defaultGraphQLSchemaFilename]
114
- const netlifyGraphRequirePath = [`./netlifyGraph`]
115
- const moduleType = baseConfig.moduleType || 'esm'
116
-
117
- return {
118
- functionsPath,
119
- webhookBasePath,
120
- netlifyGraphPath,
121
- netlifyGraphImplementationFilename,
122
- netlifyGraphTypeDefinitionsFilename,
123
- graphQLOperationsSourceFilename,
124
- graphQLSchemaFilename,
125
- netlifyGraphRequirePath,
126
- moduleType,
127
- }
128
- }
129
-
130
- /**
131
- * Return the default Netlify Graph configuration for a Nextjs site
132
- * @param {object} input
133
- * @param {object} input.baseConfig
134
- * @param {string[]} input.detectedFunctionsPath
135
- * @param {string[]} input.siteRoot
136
- */
137
- const makeDefaultNextJsNetlifyGraphConfig = ({ baseConfig, siteRoot }) => {
138
- const functionsPath = filterRelativePathItems([...siteRoot, 'pages', 'api'])
139
- const webhookBasePath = '/api'
140
- const netlifyGraphPath = filterRelativePathItems([...siteRoot, 'lib', 'netlifyGraph'])
141
- const netlifyGraphImplementationFilename = [...netlifyGraphPath, `index.${baseConfig.extension}`]
142
- const netlifyGraphTypeDefinitionsFilename = [...netlifyGraphPath, `index.d.ts`]
143
- const graphQLOperationsSourceFilename = [...netlifyGraphPath, NetlifyGraph.defaultSourceOperationsFilename]
144
- const graphQLSchemaFilename = [...netlifyGraphPath, NetlifyGraph.defaultGraphQLSchemaFilename]
145
- const netlifyGraphRequirePath = ['..', '..', 'lib', 'netlifyGraph']
146
- const moduleType = baseConfig.moduleType || 'esm'
147
-
148
- return {
149
- functionsPath,
150
- webhookBasePath,
151
- netlifyGraphPath,
152
- netlifyGraphImplementationFilename,
153
- netlifyGraphTypeDefinitionsFilename,
154
- graphQLOperationsSourceFilename,
155
- graphQLSchemaFilename,
156
- netlifyGraphRequirePath,
157
- moduleType,
158
- }
159
- }
160
-
161
- /**
162
- * Return the default Netlify Graph configuration for a Remix site
163
- * @param {object} input
164
- * @param {object} input.baseConfig
165
- * @param {string[]} input.detectedFunctionsPath
166
- * @param {string[]} input.siteRoot
167
- */
168
- const makeDefaultRemixNetlifyGraphConfig = ({ baseConfig, detectedFunctionsPath, siteRoot }) => {
169
- const functionsPath = filterRelativePathItems([...detectedFunctionsPath])
170
- const webhookBasePath = '/webhooks'
171
- const netlifyGraphPath = filterRelativePathItems([
172
- ...siteRoot,
173
- ...NetlifyGraph.defaultNetlifyGraphConfig.netlifyGraphPath,
174
- ])
175
- const netlifyGraphImplementationFilename = [...netlifyGraphPath, `index.${baseConfig.extension}`]
176
- const netlifyGraphTypeDefinitionsFilename = [...netlifyGraphPath, `index.d.ts`]
177
- const graphQLOperationsSourceFilename = [...netlifyGraphPath, NetlifyGraph.defaultSourceOperationsFilename]
178
- const graphQLSchemaFilename = [...netlifyGraphPath, NetlifyGraph.defaultGraphQLSchemaFilename]
179
- const netlifyGraphRequirePath = [`../../netlify/functions/netlifyGraph`]
180
- const moduleType = 'esm'
181
-
182
- return {
183
- functionsPath,
184
- webhookBasePath,
185
- netlifyGraphPath,
186
- netlifyGraphImplementationFilename,
187
- netlifyGraphTypeDefinitionsFilename,
188
- graphQLOperationsSourceFilename,
189
- graphQLSchemaFilename,
190
- netlifyGraphRequirePath,
191
- moduleType,
192
- }
193
- }
194
-
195
- const defaultFrameworkLookup = {
196
- 'Next.js': makeDefaultNextJsNetlifyGraphConfig,
197
- Remix: makeDefaultRemixNetlifyGraphConfig,
198
- default: makeDefaultNetlifyGraphConfig,
199
- }
200
-
201
- /**
202
- * Return a full NetlifyGraph config with any defaults overridden by netlify.toml
203
- * @param {object} input
204
- * @param {import('../../commands/base-command.mjs').default} input.command
205
- * @param {import('commander').CommandOptions} input.options
206
- * @param {Partial<import('../../utils/types').ServerSettings>=} input.settings
207
- * @return {Promise<NetlifyGraph.NetlifyGraphConfig>} NetlifyGraphConfig
208
- */
209
- const getNetlifyGraphConfig = async ({ command, options, settings }) => {
210
- const { config, site } = command.netlify
211
- config.dev = { ...config.dev }
212
- config.build = { ...config.build }
213
- const userSpecifiedConfig = (config && config.graph) || {}
214
- /** @type {import('../../commands/dev/types').DevConfig} */
215
- const devConfig = {
216
- framework: '#auto',
217
- ...(config.functionsDirectory && { functions: config.functionsDirectory }),
218
- ...(config.build.publish && { publish: config.build.publish }),
219
- ...config.dev,
220
- ...options,
221
- }
222
-
223
- /** @type {Partial<import('../../utils/types').ServerSettings>} */
224
- if (!settings) {
225
- try {
226
- settings = await detectServerSettings(devConfig, options, site.root)
227
- } catch (detectServerSettingsError) {
228
- settings = {}
229
- warn(
230
- `Error while auto-detecting project settings, Netlify Graph encountered problems: ${JSON.stringify(
231
- detectServerSettingsError,
232
- null,
233
- 2,
234
- )}`,
235
- )
236
- }
237
- }
238
-
239
- const defaulFunctionsPath = ['netlify', 'functions']
240
-
241
- const siteRoot = [path.sep, ...filterRelativePathItems(site.root.split(path.sep))]
242
-
243
- const tsConfig = 'tsconfig.json'
244
- const autodetectedLanguage = fs.existsSync(tsConfig) ? 'typescript' : 'javascript'
245
-
246
- const framework = settings.framework || userSpecifiedConfig.framework
247
- const makeDefaultFrameworkConfig = defaultFrameworkLookup[framework] || defaultFrameworkLookup.default
248
-
249
- const detectedFunctionsPathString = getFunctionsDir({ config, options })
250
- const detectedFunctionsPath = detectedFunctionsPathString
251
- ? [path.sep, ...detectedFunctionsPathString.split(path.sep)]
252
- : defaulFunctionsPath
253
- const baseConfig = { ...NetlifyGraph.defaultNetlifyGraphConfig, ...userSpecifiedConfig }
254
- const defaultFrameworkConfig = makeDefaultFrameworkConfig({ baseConfig, detectedFunctionsPath, siteRoot })
255
-
256
- const userSpecifiedFunctionPath =
257
- userSpecifiedConfig.functionsPath && userSpecifiedConfig.functionsPath.split(path.sep)
258
-
259
- const functionsPath =
260
- (userSpecifiedFunctionPath && [...siteRoot, ...userSpecifiedFunctionPath]) || defaultFrameworkConfig.functionsPath
261
- const netlifyGraphPath =
262
- (userSpecifiedConfig.netlifyGraphPath && userSpecifiedConfig.netlifyGraphPath.split(path.sep)) ||
263
- defaultFrameworkConfig.netlifyGraphPath
264
- const netlifyGraphImplementationFilename =
265
- (userSpecifiedConfig.netlifyGraphImplementationFilename &&
266
- userSpecifiedConfig.netlifyGraphImplementationFilename.split(path.sep)) ||
267
- defaultFrameworkConfig.netlifyGraphImplementationFilename
268
- const netlifyGraphTypeDefinitionsFilename =
269
- (userSpecifiedConfig.netlifyGraphTypeDefinitionsFilename &&
270
- userSpecifiedConfig.netlifyGraphTypeDefinitionsFilename.split(path.sep)) ||
271
- defaultFrameworkConfig.netlifyGraphTypeDefinitionsFilename
272
- const graphQLOperationsSourceFilename =
273
- (userSpecifiedConfig.graphQLOperationsSourceFilename &&
274
- userSpecifiedConfig.graphQLOperationsSourceFilename.split(path.sep)) ||
275
- defaultFrameworkConfig.graphQLOperationsSourceFilename
276
- const graphQLConfigJsonFilename =
277
- (userSpecifiedConfig.graphQLConfigJsonFilename && userSpecifiedConfig.graphQLConfigJsonFilename.split(path.sep)) ||
278
- defaultFrameworkConfig.graphQLConfigJsonFilename ||
279
- baseConfig.graphQLConfigJsonFilename
280
- const graphQLSchemaFilename =
281
- (userSpecifiedConfig.graphQLSchemaFilename && userSpecifiedConfig.graphQLSchemaFilename.split(path.sep)) ||
282
- defaultFrameworkConfig.graphQLSchemaFilename
283
- const netlifyGraphRequirePath =
284
- (userSpecifiedConfig.netlifyGraphRequirePath && userSpecifiedConfig.netlifyGraphRequirePath.split(path.sep)) ||
285
- defaultFrameworkConfig.netlifyGraphRequirePath
286
- const moduleType =
287
- (userSpecifiedConfig.moduleType && userSpecifiedConfig.moduleType.split(path.sep)) ||
288
- defaultFrameworkConfig.moduleType
289
- const language = userSpecifiedConfig.language || autodetectedLanguage
290
- const webhookBasePath =
291
- (userSpecifiedConfig.webhookBasePath && userSpecifiedConfig.webhookBasePath.split(path.sep)) ||
292
- defaultFrameworkConfig.webhookBasePath
293
- const customGeneratorFile =
294
- userSpecifiedConfig.customGeneratorFile && userSpecifiedConfig.customGeneratorFile.split(path.sep)
295
- const runtimeTargetEnv = userSpecifiedConfig.runtimeTargetEnv || defaultFrameworkConfig.runtimeTargetEnv || 'node'
296
-
297
- const fullConfig = {
298
- ...baseConfig,
299
- functionsPath,
300
- webhookBasePath,
301
- netlifyGraphPath,
302
- netlifyGraphImplementationFilename,
303
- netlifyGraphTypeDefinitionsFilename,
304
- graphQLOperationsSourceFilename,
305
- graphQLSchemaFilename,
306
- graphQLConfigJsonFilename,
307
- netlifyGraphRequirePath,
308
- framework,
309
- language,
310
- moduleType,
311
- customGeneratorFile,
312
- runtimeTargetEnv,
313
- }
314
-
315
- return fullConfig
316
- }
317
-
318
- /**
319
- * Given a NetlifyGraphConfig, ensure that the netlifyGraphPath exists
320
- * @param {NetlifyGraph.NetlifyGraphConfig} netlifyGraphConfig
321
- */
322
- const ensureNetlifyGraphPath = (netlifyGraphConfig) => {
323
- const fullPath = path.resolve(...netlifyGraphConfig.netlifyGraphPath)
324
- fs.mkdirSync(fullPath, { recursive: true })
325
- }
326
-
327
- /**
328
- * Given a NetlifyGraphConfig, ensure that the functionsPath exists
329
- * @param {NetlifyGraph.NetlifyGraphConfig} netlifyGraphConfig
330
- */
331
- const ensureFunctionsPath = (netlifyGraphConfig) => {
332
- const fullPath = path.resolve(...netlifyGraphConfig.functionsPath)
333
- fs.mkdirSync(fullPath, { recursive: true })
334
- }
335
-
336
- let disablePrettierDueToPreviousError = false
337
-
338
- const runPrettier = async (filePath) => {
339
- if (disablePrettierDueToPreviousError) {
340
- return
341
- }
342
-
343
- try {
344
- const commandProcess = execa('prettier', ['--write', filePath], {
345
- preferLocal: true,
346
- // windowsHide needs to be false for child process to terminate properly on Windows
347
- windowsHide: false,
348
- })
349
-
350
- await commandProcess
351
- // eslint-disable-next-line unicorn/prefer-optional-catch-binding
352
- } catch (prettierError) {
353
- // It would be nice to log this error to help debugging, but it's potentially a bit scary for the dev to see it
354
- if (!disablePrettierDueToPreviousError) {
355
- disablePrettierDueToPreviousError = true
356
- warn("Error while running prettier, make sure you have installed it globally with 'npm i -g prettier'")
357
- }
358
- }
359
- }
360
-
361
- /**
362
- * Generate a library file with type definitions for a given NetlifyGraphConfig, operationsDoc, and schema, returning the in-memory source
363
- * @param {object} input
364
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig
365
- * @param {GraphQL.GraphQLSchema} input.schema The schema to use when generating the functions and their types
366
- * @param {string} input.schemaId The id of the schema to use when fetching Graph data
367
- * @param {string} input.operationsDoc The GraphQL operations doc to use when generating the functions
368
- * @param {Record<string, NetlifyGraph.ExtractedFunction>} input.functions The parsed queries with metadata to use when generating library functions
369
- * @param {Record<string, NetlifyGraph.ExtractedFragment>} input.fragments The parsed queries with metadata to use when generating library functions
370
- * @param {import('netlify-onegraph-internal').CodegenHelpers.GenerateRuntimeFunction} input.generate
371
- * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
372
- * @returns {Promise<import('netlify-onegraph-internal').CodegenHelpers.NamedExportedFile[]>} In-memory files
373
- */
374
- const generateRuntimeSource = async ({
375
- fragments,
376
- functions,
377
- generate,
378
- netlifyGraphConfig,
379
- operationsDoc,
380
- schema,
381
- schemaId,
382
- }) => {
383
- const runtime = await NetlifyGraph.generateRuntime({
384
- GraphQL,
385
- netlifyGraphConfig,
386
- schema,
387
- operationsDoc,
388
- operations: functions,
389
- fragments,
390
- generate,
391
- schemaId,
392
- })
393
-
394
- return runtime
395
- }
396
-
397
- /**
398
- * Generate a library file with type definitions for a given NetlifyGraphConfig, operationsDoc, and schema, writing them to the filesystem
399
- * @param {object} input
400
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig
401
- * @param {GraphQL.GraphQLSchema} input.schema The schema to use when generating the functions and their types
402
- * @param {string} input.schemaId The id of the schema to use when fetching Graph data
403
- * @param {string} input.operationsDoc The GraphQL operations doc to use when generating the functions
404
- * @param {Record<string, NetlifyGraph.ExtractedFunction>} input.functions The parsed queries with metadata to use when generating library functions
405
- * @param {Record<string, NetlifyGraph.ExtractedFragment>} input.fragments The parsed queries with metadata to use when generating library functions
406
- * @param {import('netlify-onegraph-internal').CodegenHelpers.GenerateRuntimeFunction} input.generate
407
- * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
408
- * @returns {Promise<void>} Void, effectfully writes the generated library to the filesystem
409
- */
410
- const generateRuntime = async ({
411
- fragments,
412
- functions,
413
- generate,
414
- logger,
415
- netlifyGraphConfig,
416
- operationsDoc,
417
- schema,
418
- schemaId,
419
- }) => {
420
- const runtime = await generateRuntimeSource({
421
- netlifyGraphConfig,
422
- schema,
423
- operationsDoc,
424
- functions,
425
- fragments,
426
- generate,
427
- schemaId,
428
- })
429
-
430
- runtime.forEach((file) => {
431
- const implementationResolvedPath = path.resolve(...file.name)
432
- fs.writeFileSync(implementationResolvedPath, file.content, 'utf8')
433
- const implementationRelativePath = path.relative(process.cwd(), implementationResolvedPath)
434
- logger && logger(`Wrote ${chalk.cyan(implementationRelativePath)}`)
435
- runPrettier(path.resolve(...file.name))
436
- })
437
- }
438
-
439
- /**
440
- * Generate a library file with type definitions for a given NetlifyGraphConfig, operationsDoc, and schema, writing them to the filesystem
441
- * @param {object} input
442
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig
443
- * @param {object} input.config The parsed netlify.toml file
444
- * @param {string} input.schemaId
445
- * @param {GraphQL.GraphQLSchema} input.schema The schema to use when generating the functions and their types
446
- * @param {string} input.operationsDoc The GraphQL operations doc to use when generating the functions
447
- * @param {Record<string, NetlifyGraph.ExtractedFunction>} input.functions The parsed queries with metadata to use when generating library functions
448
- * @param {Record<string, NetlifyGraph.ExtractedFragment>} input.fragments The parsed queries with metadata to use when generating library functions
449
- * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
450
- * @returns {Promise<void>} Void, effectfully writes the generated library to the filesystem
451
- */
452
- const generateFunctionsFile = async ({ config, netlifyGraphConfig, operationsDoc, schema, schemaId }) => {
453
- const parsedDoc = GraphQL.parse(operationsDoc)
454
-
455
- const extracted = extractFunctionsFromOperationDoc(GraphQL, parsedDoc)
456
-
457
- const codegenModule = await getCodegenModule({ config })
458
- if (!codegenModule) {
459
- warn(
460
- `No Netlify Graph codegen module specified in netlify.toml under the [graph] header. Please specify 'codeGenerator' field and try again.`,
461
- )
462
- return
463
- }
464
-
465
- await generateRuntime({
466
- generate: codegenModule.generateRuntime,
467
- schema,
468
- schemaId,
469
- netlifyGraphConfig,
470
- logger: log,
471
- fragments: extracted.fragments,
472
- functions: extracted.functions,
473
- operationsDoc,
474
- })
475
- }
476
-
477
- /**
478
- * Using the given NetlifyGraphConfig, read the GraphQL operations file and return the _unparsed_ GraphQL operations doc
479
- * @param {NetlifyGraph.NetlifyGraphConfig} netlifyGraphConfig
480
- * @returns {string} GraphQL operations doc
481
- */
482
- const readGraphQLOperationsSourceFile = (netlifyGraphConfig) => {
483
- ensureNetlifyGraphPath(netlifyGraphConfig)
484
-
485
- const fullFilename = path.resolve(...(netlifyGraphConfig.graphQLOperationsSourceFilename || []))
486
- if (!fs.existsSync(fullFilename)) {
487
- fs.writeFileSync(fullFilename, '')
488
- fs.closeSync(fs.openSync(fullFilename, 'w'))
489
- }
490
-
491
- const source = fs.readFileSync(fullFilename, 'utf8')
492
-
493
- return source
494
- }
495
-
496
- /**
497
- * Write an operations doc to the filesystem using the given NetlifyGraphConfig
498
- * @param {object} input
499
- * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
500
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig
501
- * @param {string} input.operationsDocString The GraphQL operations doc to write
502
- */
503
- const writeGraphQLOperationsSourceFile = ({ logger, netlifyGraphConfig, operationsDocString }) => {
504
- const graphqlSource = operationsDocString
505
-
506
- ensureNetlifyGraphPath(netlifyGraphConfig)
507
- const resolvedPath = path.resolve(...(netlifyGraphConfig.graphQLOperationsSourceFilename || []))
508
- fs.writeFileSync(resolvedPath, graphqlSource, 'utf8')
509
- const relativePath = path.relative(process.cwd(), resolvedPath)
510
- logger && logger(`Wrote ${chalk.cyan(relativePath)}`)
511
- }
512
-
513
- /**
514
- * Write a GraphQL Schema printed in SDL format to the filesystem using the given NetlifyGraphConfig
515
- * @param {object} input
516
- * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
517
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig
518
- * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to print and write to the filesystem
519
- */
520
- const writeGraphQLSchemaFile = ({ logger, netlifyGraphConfig, schema }) => {
521
- const graphqlSource = printSchema(schema)
522
-
523
- ensureNetlifyGraphPath(netlifyGraphConfig)
524
- const resolvedPath = path.resolve(...netlifyGraphConfig.graphQLSchemaFilename)
525
- fs.writeFileSync(resolvedPath, graphqlSource, 'utf8')
526
- const relativePath = path.relative(process.cwd(), resolvedPath)
527
- logger && logger(`Wrote ${chalk.cyan(relativePath)}`)
528
- }
529
-
530
- /**
531
- * Using the given NetlifyGraphConfig, read the GraphQL schema file and return it _unparsed_
532
- * @param {NetlifyGraph.NetlifyGraphConfig} netlifyGraphConfig
533
- * @returns {string} GraphQL schema
534
- */
535
- const readGraphQLSchemaFile = (netlifyGraphConfig) => {
536
- ensureNetlifyGraphPath(netlifyGraphConfig)
537
- return fs.readFileSync(path.resolve(...netlifyGraphConfig.graphQLSchemaFilename), 'utf8')
538
- }
539
-
540
- /**
541
- * Given a NetlifyGraphConfig, read the appropriate files and write a handler for the given operationId to the filesystem
542
- * @param {object} input
543
- * @param {import('netlify-onegraph-internal').CodegenHelpers.GenerateHandlerFunction} input.generate
544
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig
545
- * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating the handler
546
- * @param {string} input.operationId The operationId to use when generating the handler
547
- * @param {string} input.operationsDoc The document containing the operation with operationId and any fragment dependency to use when generating the handler
548
- * @param {object} input.handlerOptions The options to use when generating the handler
549
- * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
550
- * @returns {Promise<{exportedFiles: import('netlify-onegraph-internal').CodegenHelpers.ExportedFile[]; operation: GraphQL.OperationDefinitionNode;} | undefined>} The generated files
551
- */
552
- const generateHandlerSourceByOperationId = async ({
553
- generate,
554
- handlerOptions,
555
- netlifyGraphConfig,
556
- operationId,
557
- operationsDoc,
558
- schema,
559
- }) => {
560
- const generateHandlerPayload = {
561
- GraphQL,
562
- generate,
563
- handlerOptions,
564
- schema,
565
- schemaString: GraphQL.printSchema(schema),
566
- netlifyGraphConfig,
567
- operationId,
568
- operationsDoc,
569
- }
570
-
571
- const result = await NetlifyGraph.generateCustomHandlerSource(generateHandlerPayload)
572
-
573
- return result
574
- }
575
-
576
- /**
577
- * Given a NetlifyGraphConfig, read the appropriate files and write a handler for the given operationId to the filesystem
578
- * @param {object} input
579
- * @param {import('netlify-onegraph-internal').CodegenHelpers.GenerateHandlerFunction} input.generate
580
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig
581
- * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating the handler
582
- * @param {string} input.operationId The operationId to use when generating the handler
583
- * @param {object} input.handlerOptions The options to use when generating the handler
584
- * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
585
- * @returns {Promise<Array<{filePath: string, name:string, prettierSuccess: boolean}> | undefined>} An array of the generated handler filepaths
586
- */
587
- const generateHandlerByOperationId = async ({ generate, handlerOptions, netlifyGraphConfig, operationId, schema }) => {
588
- let currentOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
589
- if (currentOperationsDoc.trim().length === 0) {
590
- currentOperationsDoc = NetlifyGraph.defaultExampleOperationsDoc
591
- }
592
-
593
- const result = await generateHandlerSourceByOperationId({
594
- generate,
595
- handlerOptions,
596
- netlifyGraphConfig,
597
- operationId,
598
- operationsDoc: currentOperationsDoc,
599
- schema,
600
- })
601
-
602
- if (!result) {
603
- warn(`No handler was generated for operationId ${operationId}`)
604
- return
605
- }
606
-
607
- const { exportedFiles, operation } = result
608
-
609
- log('Ensure destination path exists...')
610
-
611
- ensureFunctionsPath(netlifyGraphConfig)
612
-
613
- if (!exportedFiles) {
614
- warn(`No exported files from Netlify Graph code generator`)
615
- return
616
- }
617
-
618
- /** @type {Array<{filePath: string, name:string, prettierSuccess: boolean}>} */
619
- const results = []
620
-
621
- exportedFiles.forEach((exportedFile) => {
622
- const { content } = exportedFile
623
- const isNamed = exportedFile.kind === 'NamedExportedFile'
624
-
625
- let filenameArr
626
-
627
- if (isNamed) {
628
- filenameArr = [...exportedFile.name]
629
- } else {
630
- const operationName = (operation.name && operation.name.value) || 'Unnamed'
631
- const fileExtension = netlifyGraphConfig.language === 'typescript' ? 'ts' : netlifyGraphConfig.extension
632
- const defaultBaseFilename = `${operationName}.${fileExtension}`
633
- const baseFilename = defaultBaseFilename
634
-
635
- filenameArr = [path.sep, ...netlifyGraphConfig.functionsPath, baseFilename]
636
- }
637
-
638
- const parentDir = path.resolve(...filterRelativePathItems(filenameArr.slice(0, -1)))
639
-
640
- // Make sure the parent directory exists
641
- fs.mkdirSync(parentDir, { recursive: true })
642
-
643
- const absoluteFilename = path.resolve(...filenameArr)
644
-
645
- fs.writeFileSync(absoluteFilename, content)
646
- const relativePath = path.relative(process.cwd(), absoluteFilename)
647
- log(`Wrote ${chalk.cyan(relativePath)}`)
648
- runPrettier(absoluteFilename)
649
-
650
- results.push({
651
- name: filenameArr.slice(-1)[0],
652
- filePath: absoluteFilename,
653
- prettierSuccess: true,
654
- })
655
- })
656
-
657
- return results
658
- }
659
-
660
- /**
661
- * Given a NetlifyGraphConfig, read the appropriate files and write a handler for the given operationId to the filesystem
662
- * @param {object} input
663
- * @param {import('netlify-onegraph-internal').CodegenHelpers.GenerateHandlerFunction} input.generate
664
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig
665
- * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating the handler
666
- * @param {string} input.operationName The name of the operation to use when generating the handler
667
- * @param {object} input.handlerOptions The options to use when generating the handler
668
- * @param {(message: string) => void} input.logger A function that if provided will be used to log messages
669
- * @returns {Promise<void>}
670
- */
671
- const generateHandlerByOperationName = async ({
672
- generate,
673
- handlerOptions,
674
- logger,
675
- netlifyGraphConfig,
676
- operationName,
677
- schema,
678
- }) => {
679
- let currentOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
680
- if (currentOperationsDoc.trim().length === 0) {
681
- currentOperationsDoc = NetlifyGraph.defaultExampleOperationsDoc
682
- }
683
-
684
- const parsedDoc = parse(currentOperationsDoc)
685
- const { fragments, functions } = extractFunctionsFromOperationDoc(GraphQL, parsedDoc)
686
-
687
- const functionDefinition = Object.values(functions).find(
688
- (potentialDefinition) => potentialDefinition.operationName === operationName,
689
- )
690
-
691
- const fragmentDefinition = Object.values(fragments).find(
692
- (potentialDefinition) => potentialDefinition.fragmentName === operationName,
693
- )
694
-
695
- const definition = functionDefinition || fragmentDefinition
696
-
697
- if (!definition) {
698
- warn(`No operation named ${operationName} was found in the operations doc`)
699
- return
700
- }
701
-
702
- await generateHandlerByOperationId({
703
- logger,
704
- generate,
705
- netlifyGraphConfig,
706
- schema,
707
- operationId: definition.id,
708
- handlerOptions,
709
- })
710
- }
711
-
712
- /**
713
- * Given a NetlifyGraphConfig, read the appropriate files and write a handler for the given operationId to the filesystem
714
- * @param {object} input
715
- * @param {import('netlify-onegraph-internal').CodegenHelpers.GenerateHandlerPreviewFunction} input.generate
716
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig
717
- * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating the handler
718
- * @param {string} input.operationName The name of the operation to use when generating the handler
719
- * @param {object} input.handlerOptions The options to use when generating the handler
720
- * @param {(message: string) => void} input.logger A function that if provided will be used to log messages
721
- * @returns {import('netlify-onegraph-internal').CodegenHelpers.ExportedFile | undefined}
722
- */
723
- const generateHandlerPreviewByOperationName = ({
724
- generate,
725
- handlerOptions,
726
- netlifyGraphConfig,
727
- operationName,
728
- schema,
729
- }) => {
730
- let currentOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
731
- if (currentOperationsDoc.trim().length === 0) {
732
- currentOperationsDoc = NetlifyGraph.defaultExampleOperationsDoc
733
- }
734
-
735
- const parsedDoc = parse(currentOperationsDoc)
736
- const { functions } = extractFunctionsFromOperationDoc(GraphQL, parsedDoc)
737
-
738
- const operation = Object.values(functions).find(
739
- (potentialOperation) => potentialOperation.operationName === operationName,
740
- )
741
-
742
- if (!operation) {
743
- warn(`No operation named ${operationName} was found in the operations doc`)
744
- return
745
- }
746
-
747
- const generateHandlerPreviewPayload = {
748
- GraphQL,
749
- generate,
750
- handlerOptions,
751
- schema,
752
- schemaString: GraphQL.printSchema(schema),
753
- netlifyGraphConfig,
754
- operationId: operation.id,
755
- operationsDoc: currentOperationsDoc,
756
- }
757
-
758
- const preview = NetlifyGraph.generatePreview(generateHandlerPreviewPayload)
759
-
760
- if (!preview) {
761
- return
762
- }
763
-
764
- return preview.exportedFile
765
- }
766
-
767
- // Export the minimal set of functions that are required for Netlify Graph
768
- const { buildSchema, parse } = GraphQL
769
-
770
- /**
771
- *
772
- * @param {object} options
773
- * @param {string} options.siteName The name of the site as used in the Netlify UI url scheme
774
- * @param {string} options.oneGraphSessionId The oneGraph session id to use when generating the graph-edit link
775
- * @returns {string} The url to the Netlify Graph UI for the current session
776
- */
777
- const getGraphEditUrlBySiteName = ({ oneGraphSessionId, siteName }) => {
778
- const host = process.env.NETLIFY_APP_HOST || 'app.netlify.com'
779
- // http because app.netlify.com will redirect to https, and localhost will still work for development
780
- const url = `http://${host}/sites/app/${siteName}/graph/explorer/${oneGraphSessionId}`
781
-
782
- return url
783
- }
784
-
785
- /**
786
- * Get a url to the Netlify Graph UI for the current session by a site's id
787
- * @param {object} options
788
- * @param {string} options.siteId The name of the site as used in the Netlify UI url scheme
789
- * @param {string} options.oneGraphSessionId The oneGraph session id to use when generating the graph-edit link
790
- * @returns {string} The url to the Netlify Graph UI for the current session
791
- */
792
- const getGraphEditUrlBySiteId = ({ oneGraphSessionId, siteId }) => {
793
- const host = process.env.NETLIFY_APP_HOST || 'app.netlify.com'
794
- // http because app.netlify.com will redirect to https, and localhost will still work for development
795
- const url = `http://${host}/site-redirect/${siteId}/graph/explorer/${oneGraphSessionId}`
796
-
797
- return url
798
- }
799
-
800
- /**
801
- * Load `netlifyGraph.json` from the appropriate location
802
- * @param {string} siteRoot The root directory of the site
803
- * @returns {import('netlify-onegraph-internal').NetlifyGraphJsonConfig.JsonConfig}
804
- */
805
- const loadNetlifyGraphConfig = (siteRoot) => {
806
- const configPath = path.join(siteRoot, 'netlifyGraph.json')
807
- if (fs.existsSync(configPath)) {
808
- // eslint-disable-next-line unicorn/prefer-json-parse-buffer
809
- const file = fs.readFileSync(configPath, 'utf-8')
810
- return JSON.parse(file)
811
- }
812
-
813
- return {}
814
- }
815
-
816
- const autocompleteOperationNames = async ({ netlifyGraphConfig }) => {
817
- try {
818
- let currentOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
819
- if (currentOperationsDoc.trim().length === 0) {
820
- currentOperationsDoc = NetlifyGraph.defaultExampleOperationsDoc
821
- }
822
-
823
- const parsedDoc = parse(currentOperationsDoc)
824
- const extracted = extractFunctionsFromOperationDoc(GraphQL, parsedDoc)
825
-
826
- const { functions } = extracted
827
-
828
- const sorted = Object.values(functions).sort((aItem, bItem) =>
829
- aItem.operationName.localeCompare(bItem.operationName),
830
- )
831
-
832
- const perPage = 50
833
-
834
- const allOperationChoices = sorted.map((operation) => ({
835
- name: `${operation.operationName} (${operation.kind})`,
836
- value: operation.operationName,
837
- }))
838
-
839
- const filterOperationNames = (operationChoices, input) =>
840
- operationChoices.filter((operation) => operation.value.toLowerCase().match(input.toLowerCase()))
841
-
842
- /** multiple matching detectors, make the user choose */
843
- // @ts-ignore
844
- inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt)
845
-
846
- // @ts-ignore
847
- const { selectedOperationName } = await inquirer.prompt({
848
- name: 'selectedOperationName',
849
- message: `For which operation would you like to generate a handler?`,
850
- type: 'autocomplete',
851
- pageSize: perPage,
852
- source(_, input) {
853
- if (!input || input === '') {
854
- return allOperationChoices
855
- }
856
-
857
- const filteredChoices = filterOperationNames(allOperationChoices, input)
858
- // only show filtered results
859
- return filteredChoices
860
- },
861
- })
862
-
863
- return selectedOperationName
864
- } catch (parseError) {
865
- error(`Error parsing operations library: ${parseError}`)
866
- }
867
- }
868
-
869
- /** @type {string | undefined} */
870
- let lastWarnedFailedCodegenModule
871
-
872
- /**
873
- * @param {object} input
874
- * @param {object} input.config The parsed netlify.toml file
875
- * @param {string=} input.cwd The optional directory to use as a base path when resolving codegen modules
876
- * @returns {Promise<import('netlify-onegraph-internal').CodegenHelpers.CodegenModule | void>} codegenModule
877
- */
878
- const dynamicallyLoadCodegenModule = async ({ config, cwd }) => {
879
- const basePath = cwd || process.cwd()
880
- const importPath = config.graph && config.graph.codeGenerator
881
-
882
- if (!importPath) {
883
- return
884
- }
885
-
886
- // We currently include some default code generators for the most common framework
887
- // use-cases. We still require it to be explicitly configured in netlify.toml,
888
- // but we don't require an additional package install for it.
889
- const includedCodegenModule = IncludedCodegen.includedCodegenModules.find(
890
- (codegenModule) => codegenModule.sigil === importPath,
891
- )
892
-
893
- if (includedCodegenModule) {
894
- return includedCodegenModule
895
- }
896
-
897
- try {
898
- if (!importPath) {
899
- warn(
900
- `No Netlify Graph codegen module specified in netlify.toml under the [graph] header. Please specify 'codeGenerator' field and try again.`,
901
- )
902
- return
903
- }
904
-
905
- const absolute = [basePath, 'node_modules', ...importPath.split('/'), 'index.js']
906
- const relativePath = path.join(basePath, importPath)
907
- const absoluteOrNodePath = path.resolve(...absolute)
908
-
909
- const finalPath = fs.existsSync(relativePath) ? relativePath : pathToFileURL(absoluteOrNodePath).href
910
-
911
- /** @type {import('netlify-onegraph-internal').CodegenHelpers.CodegenModule | undefined} */
912
- // eslint-disable-next-line import/no-dynamic-require
913
- const newModule = await import(finalPath)
914
-
915
- if (newModule) {
916
- const hasGenerators = Array.isArray(newModule.generators)
917
- let generatorsConform = true
918
- if (hasGenerators) {
919
- newModule.generators.forEach((generator) => {
920
- const hasId = Boolean(generator.id)
921
- const hasName = Boolean(generator.name)
922
- const hasVersion = Boolean(generator.version)
923
- const hasGenerator = Boolean(generator.generateHandler)
924
-
925
- if (!hasId || !hasName || !hasVersion || !hasGenerator) {
926
- generatorsConform = false
927
- }
928
- })
929
- }
930
-
931
- if (!generatorsConform) {
932
- warn(`Generator ${importPath} does not conform to expected module declaration type, ignoring...`)
933
- return
934
- }
935
- }
936
-
937
- return newModule
938
- } catch (error_) {
939
- if (lastWarnedFailedCodegenModule !== importPath) {
940
- warn(`Failed to load Graph code generator, please make sure that "${importPath}" either exists as a local file or is listed in your package.json under devDependencies, and can be 'require'd or 'import'ed.
941
-
942
- ${error_}`)
943
-
944
- lastWarnedFailedCodegenModule = importPath
945
- }
946
- }
947
- }
948
-
949
- const getCodegenModule = ({ config }) => dynamicallyLoadCodegenModule({ config })
950
-
951
- const getCodegenFunctionById = async ({ config, id }) => {
952
- const codegenModule = await getCodegenModule({ config })
953
-
954
- return codegenModule && codegenModule.generators && codegenModule.generators.find((generator) => generator.id === id)
955
- }
956
-
957
- const autocompleteCodegenModules = async ({ config }) => {
958
- const codegenModule = await getCodegenModule({ config })
959
- if (!codegenModule || !codegenModule.generators) {
960
- return null
961
- }
962
-
963
- log(`Using Graph Codegen module ${codegenModule.id} [${codegenModule.version}] from '${config.graph.codeGenerator}'`)
964
-
965
- const allGeneratorChoices = codegenModule.generators
966
- // eslint-disable-next-line id-length
967
- .sort((a, b) => a.name.localeCompare(b.name))
968
- .map((codeGen) => ({
969
- name: `${codeGen.name} [${codeGen.id}]`,
970
- value: codeGen.name,
971
- }))
972
-
973
- // eslint-disable-next-line unicorn/consistent-function-scoping
974
- const filterModuleNames = (moduleChoices, input) =>
975
- moduleChoices.filter((moduleChoice) => moduleChoice.name.toLowerCase().match(input.toLowerCase()))
976
-
977
- /** multiple matching detectors, make the user choose */
978
- // @ts-ignore
979
- inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt)
980
-
981
- const perPage = 50
982
-
983
- // @ts-ignore
984
- const { selectedCodeGen } = await inquirer.prompt({
985
- name: 'selectedCodeGen',
986
- message: `Which codegen would you like to use?`,
987
- type: 'autocomplete',
988
- pageSize: perPage,
989
- source(_, input) {
990
- if (!input || input === '') {
991
- return allGeneratorChoices
992
- }
993
-
994
- const filteredChoices = filterModuleNames(allGeneratorChoices, input)
995
- // only show filtered results
996
- return filteredChoices
997
- },
998
- })
999
-
1000
- return codegenModule.generators.find(
1001
- (dynamicallyImportedModule) => dynamicallyImportedModule.name === selectedCodeGen,
1002
- )
1003
- }
1004
-
1005
- export {
1006
- autocompleteCodegenModules,
1007
- autocompleteOperationNames,
1008
- buildSchema,
1009
- defaultExampleOperationsDoc,
1010
- extractFunctionsFromOperationDoc,
1011
- generateFunctionsFile,
1012
- generateHandlerSource,
1013
- generateHandlerByOperationId,
1014
- generateHandlerByOperationName,
1015
- generateHandlerPreviewByOperationName,
1016
- generateHandlerSourceByOperationId,
1017
- generateRuntime,
1018
- generateRuntimeSource,
1019
- getCodegenFunctionById,
1020
- getCodegenModule,
1021
- getGraphEditUrlBySiteId,
1022
- getGraphEditUrlBySiteName,
1023
- getNetlifyGraphConfig,
1024
- loadNetlifyGraphConfig,
1025
- normalizeOperationsDoc,
1026
- parse,
1027
- readGraphQLOperationsSourceFile,
1028
- readGraphQLSchemaFile,
1029
- setNetlifyTomlCodeGeneratorModule,
1030
- runPrettier,
1031
- writeGraphQLOperationsSourceFile,
1032
- writeGraphQLSchemaFile,
1033
- }