Skip to content

Commit

Permalink
feat: Show edit graph link on dev startup, send operations library ed…
Browse files Browse the repository at this point in the history
…its to UI, and run prettier (if available) on code output (#4183)

* feat: show graph edit link on dev startup, send ops-lib edits to UI, & prettier code output

* chore: make eslint happy

* chore: fix formatting
  • Loading branch information
sgrove committed Feb 5, 2022
1 parent 0577c15 commit effffec
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 30 deletions.
38 changes: 34 additions & 4 deletions src/commands/dev/dev.js
Expand Up @@ -11,8 +11,18 @@ const stripAnsiCc = require('strip-ansi-control-characters')
const waitPort = require('wait-port')

const { startFunctionsServer } = require('../../lib/functions/server')
const { OneGraphCliClient, startOneGraphCLISession } = require('../../lib/one-graph/cli-client')
const { getNetlifyGraphConfig } = require('../../lib/one-graph/cli-netlify-graph')
const {
OneGraphCliClient,
loadCLISession,
persistNewOperationsDocForSession,
startOneGraphCLISession,
} = require('../../lib/one-graph/cli-client')
const {
defaultExampleOperationsDoc,
getGraphEditUrlBySiteId,
getNetlifyGraphConfig,
readGraphQLOperationsSourceFile,
} = require('../../lib/one-graph/cli-netlify-graph')
const {
NETLIFYDEV,
NETLIFYDEVERR,
Expand Down Expand Up @@ -349,9 +359,29 @@ const dev = async (options, command) => {
await OneGraphCliClient.ensureAppForSite(netlifyToken, site.id)
const netlifyGraphConfig = await getNetlifyGraphConfig({ command, options, settings })

log(`Starting Netlify Graph session, to edit your library run \`netlify graph:edit\` in another tab`)
let graphqlDocument = readGraphQLOperationsSourceFile(netlifyGraphConfig)

if (!graphqlDocument || graphqlDocument.trim().length === 0) {
graphqlDocument = defaultExampleOperationsDoc
}

await startOneGraphCLISession({ netlifyGraphConfig, netlifyToken, site, state })

// Should be created by startOneGraphCLISession
const oneGraphSessionId = loadCLISession(state)

startOneGraphCLISession({ netlifyGraphConfig, netlifyToken, site, state })
await persistNewOperationsDocForSession({
netlifyToken,
oneGraphSessionId,
operationsDoc: graphqlDocument,
siteId: site.id,
})

const graphEditUrl = getGraphEditUrlBySiteId({ siteId: site.id, oneGraphSessionId })

log(
`Starting Netlify Graph session, to edit your library visit ${graphEditUrl} or run \`netlify graph:edit\` in another tab`,
)
}

printBanner({ url })
Expand Down
16 changes: 3 additions & 13 deletions src/commands/graph/graph-edit.js
Expand Up @@ -3,7 +3,7 @@ const gitRepoInfo = require('git-repo-info')
const { OneGraphCliClient, generateSessionName, loadCLISession } = require('../../lib/one-graph/cli-client')
const {
defaultExampleOperationsDoc,
getGraphEditUrlBySiteName,
getGraphEditUrlBySiteId,
getNetlifyGraphConfig,
readGraphQLOperationsSourceFile,
} = require('../../lib/one-graph/cli-netlify-graph')
Expand All @@ -19,7 +19,7 @@ const { createCLISession, createPersistedQuery, ensureAppForSite, updateCLISessi
* @returns
*/
const graphEdit = async (options, command) => {
const { api, site, siteInfo, state } = command.netlify
const { site, state } = command.netlify
const siteId = site.id

if (!site.id) {
Expand Down Expand Up @@ -60,17 +60,7 @@ const graphEdit = async (options, command) => {

await updateCLISessionMetadata(netlifyToken, siteId, oneGraphSessionId, { docId: persistedDoc.id })

let siteName = siteInfo.name

if (!siteName) {
const siteData = await api.getSite({ siteId })
siteName = siteData.name
if (!siteName) {
error(`No site name found for siteId ${siteId}`)
}
}

const graphEditUrl = getGraphEditUrlBySiteName({ siteName, oneGraphSessionId })
const graphEditUrl = getGraphEditUrlBySiteId({ siteId, oneGraphSessionId })

await openBrowser({ url: graphEditUrl })
}
Expand Down
143 changes: 131 additions & 12 deletions src/lib/one-graph/cli-client.js
@@ -1,13 +1,15 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable fp/no-loops */
const crypto = require('crypto')
const os = require('os')
const path = require('path')

const gitRepoInfo = require('git-repo-info')
const { GraphQL, InternalConsole, OneGraphClient } = require('netlify-onegraph-internal')
const { NetlifyGraph } = require('netlify-onegraph-internal')

const { chalk, error, log, warn } = require('../../utils')

const { createCLISession, createPersistedQuery, ensureAppForSite, updateCLISessionMetadata } = OneGraphClient
const { watchDebounced } = require('../functions/watcher')

const {
generateFunctionsFile,
Expand All @@ -19,6 +21,7 @@ const {

const { parse } = GraphQL
const { defaultExampleOperationsDoc, extractFunctionsFromOperationDoc } = NetlifyGraph
const { createCLISession, createPersistedQuery, ensureAppForSite, updateCLISessionMetadata } = OneGraphClient

const internalConsole = {
log,
Expand All @@ -27,6 +30,9 @@ const internalConsole = {
debug: console.debug,
}

const witnessedIncomingDocumentHashes = []

// Keep track of which document hashes we've received from the server so we can ignore events from the filesystem based on them
InternalConsole.registerConsole(internalConsole)

/**
Expand Down Expand Up @@ -108,6 +114,26 @@ const monitorCLISessionEvents = (input) => {
return close
}

/**
* Monitor the operations document for changes
* @param {object} input
* @param {NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
* @param {function} input.onAdd A callback function to handle when the operations document is added
* @param {function} input.onChange A callback function to handle when the operations document is changed
* @param {function} input.onUnlink A callback function to handle when the operations document is unlinked
* @returns {Promise<watcher>}
*/
const monitorOperationFile = async ({ netlifyGraphConfig, onAdd, onChange, onUnlink }) => {
const filePath = path.resolve(...netlifyGraphConfig.graphQLOperationsSourceFilename)
const newWatcher = await watchDebounced([filePath], {
onAdd,
onChange,
onUnlink,
})

return newWatcher
}

/**
* Fetch the schema for a site, and regenerate all of the downstream files
* @param {object} input
Expand Down Expand Up @@ -146,7 +172,44 @@ const refetchAndGenerateFromOneGraph = async (input) => {
}

/**
*
* Regenerate the function library based on the current operations document on disk
* @param {object} input
* @param {string} input.schema The GraphQL schema to use when generating code
* @param {NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
* @returns
*/
const regenerateFunctionsFileFromOperationsFile = (input) => {
const { netlifyGraphConfig, schema } = input

const appOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)

const hash = quickHash(appOperationsDoc)

if (witnessedIncomingDocumentHashes.includes(hash)) {
// We've already seen this document, so don't regenerate
return
}

const parsedDoc = parse(appOperationsDoc, {
noLocation: true,
})
const { fragments, functions } = extractFunctionsFromOperationDoc(parsedDoc)
generateFunctionsFile({ netlifyGraphConfig, schema, operationsDoc: appOperationsDoc, functions, fragments })
}

/**
* Compute a md5 hash of a string
* @param {string} input String to compute a quick md5 hash for
* @returns hex digest of the input string
*/
const quickHash = (input) => {
const hashSum = crypto.createHash('md5')
hashSum.update(input)
return hashSum.digest('hex')
}

/**
* Fetch a persisted operations doc by its id, write it to the system, and regenerate the library
* @param {object} input
* @param {string} input.siteId The site id to query against
* @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
Expand All @@ -155,7 +218,7 @@ const refetchAndGenerateFromOneGraph = async (input) => {
* @param {NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
* @returns
*/
const updateGraphQLOperationsFile = async (input) => {
const updateGraphQLOperationsFileFromPersistedDoc = async (input) => {
const { docId, netlifyGraphConfig, netlifyToken, schema, siteId } = input
const persistedDoc = await OneGraphClient.fetchPersistedQuery(netlifyToken, siteId, docId)
if (!persistedDoc) {
Expand All @@ -166,12 +229,17 @@ const updateGraphQLOperationsFile = async (input) => {
const doc = persistedDoc.query

writeGraphQLOperationsSourceFile(netlifyGraphConfig, doc)
const appOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
const parsedDoc = parse(appOperationsDoc, {
noLocation: true,
})
const { fragments, functions } = extractFunctionsFromOperationDoc(parsedDoc)
generateFunctionsFile({ netlifyGraphConfig, schema, operationsDoc: appOperationsDoc, functions, fragments })
regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })

const hash = quickHash(doc)

const relevantHasLength = 10

if (witnessedIncomingDocumentHashes.length > relevantHasLength) {
witnessedIncomingDocumentHashes.shift()
}

witnessedIncomingDocumentHashes.push(hash)
}

const handleCliSessionEvent = async ({ event, netlifyGraphConfig, netlifyToken, schema, siteId }) => {
Expand All @@ -184,7 +252,13 @@ const handleCliSessionEvent = async ({ event, netlifyGraphConfig, netlifyToken,
await generateHandler(netlifyGraphConfig, schema, payload.operationId, payload)
break
case 'OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent':
await updateGraphQLOperationsFile({ netlifyToken, docId: payload.docId, netlifyGraphConfig, schema, siteId })
await updateGraphQLOperationsFileFromPersistedDoc({
netlifyToken,
docId: payload.docId,
netlifyGraphConfig,
schema,
siteId,
})
break
default: {
warn(`Unrecognized event received, you may need to upgrade your CLI version`, __typename, payload)
Expand All @@ -193,6 +267,24 @@ const handleCliSessionEvent = async ({ event, netlifyGraphConfig, netlifyToken,
}
}

const persistNewOperationsDocForSession = async ({ netlifyToken, oneGraphSessionId, operationsDoc, siteId }) => {
const { branch } = gitRepoInfo()

const payload = {
appId: siteId,
description: 'Temporary snapshot of local queries',
document: operationsDoc,
tags: ['netlify-cli', `session:${oneGraphSessionId}`, `git-branch:${branch}`, `local-change`],
}
const persistedDoc = await createPersistedQuery(netlifyToken, payload)
const newMetadata = await { docId: persistedDoc.id }
const result = await OneGraphClient.updateCLISessionMetadata(netlifyToken, siteId, oneGraphSessionId, newMetadata)

if (result.errors) {
warn('Unable to update session metadata with updated operations doc', result.errors)
}
}

/**
* Load the CLI session id from the local state
* @param {state} state
Expand All @@ -201,7 +293,7 @@ const handleCliSessionEvent = async ({ event, netlifyGraphConfig, netlifyToken,
const loadCLISession = (state) => state.get('oneGraphSessionId')

/**
* Idemponentially save the CLI session id to the local state and start monitoring for CLI events and upstream schema changes
* Idemponentially save the CLI session id to the local state and start monitoring for CLI events, upstream schema changes, and local operation file changes
* @param {object} input
* @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
* @param {NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
Expand All @@ -223,6 +315,32 @@ const startOneGraphCLISession = async (input) => {
const enabledServices = []
const schema = await OneGraphClient.fetchOneGraphSchema(site.id, enabledServices)

monitorOperationFile({
netlifyGraphConfig,
onChange: async (filePath) => {
log('NetlifyGraph operation file changed at', filePath, 'updating function library...')
regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })
const newOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
await persistNewOperationsDocForSession({
netlifyToken,
oneGraphSessionId,
operationsDoc: newOperationsDoc,
siteId: site.id,
})
},
onAdd: async (filePath) => {
log('NetlifyGraph operation file created at', filePath, 'creating function library...')
regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })
const newOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
await persistNewOperationsDocForSession({
netlifyToken,
oneGraphSessionId,
operationsDoc: newOperationsDoc,
siteId: site.id,
})
},
})

monitorCLISessionEvents({
appId: site.id,
netlifyToken,
Expand Down Expand Up @@ -273,6 +391,7 @@ module.exports = {
generateSessionName,
loadCLISession,
monitorCLISessionEvents,
persistNewOperationsDocForSession,
refetchAndGenerateFromOneGraph,
startOneGraphCLISession,
}

1 comment on commit effffec

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📊 Benchmark results

Package size: 362 MB

Please sign in to comment.