From 0dc738df6ef7d9c5d1fd32e65495cec84d33576a Mon Sep 17 00:00:00 2001 From: sw-yx Date: Fri, 22 Mar 2019 05:27:05 -0400 Subject: [PATCH] break up the registry into indiviudal files --- README.md | 14 ++ src/commands/functions/create.js | 151 ++++++++++++------ .../.netlify-function-template.js | 7 + .../auth-fetch/.netlify-function-template.js | 10 ++ .../fauna-crud/.netlify-function-template.js | 8 + .../hello-world/.netlify-function-template.js | 5 + .../node-fetch/.netlify-function-template.js | 7 + .../.netlify-function-template.js | 7 + .../.netlify-function-template.js | 7 + .../set-cookie/.netlify-function-template.js | 7 + .../js/template-registry.js | 72 --------- .../.netlify-function-template.js | 7 + .../unused_go/template-registry.js | 12 -- .../unused_ts/template-registry.js | 19 --- src/utils/readRepoURL.js | 13 +- 15 files changed, 191 insertions(+), 155 deletions(-) create mode 100644 src/functions-templates/js/apollo-graphql/.netlify-function-template.js create mode 100644 src/functions-templates/js/auth-fetch/.netlify-function-template.js create mode 100644 src/functions-templates/js/fauna-crud/.netlify-function-template.js create mode 100644 src/functions-templates/js/hello-world/.netlify-function-template.js create mode 100644 src/functions-templates/js/node-fetch/.netlify-function-template.js create mode 100644 src/functions-templates/js/protected-function/.netlify-function-template.js create mode 100644 src/functions-templates/js/serverless-ssr/.netlify-function-template.js create mode 100644 src/functions-templates/js/set-cookie/.netlify-function-template.js delete mode 100644 src/functions-templates/js/template-registry.js create mode 100644 src/functions-templates/js/using-middleware/.netlify-function-template.js delete mode 100644 src/functions-templates/unused_go/template-registry.js delete mode 100644 src/functions-templates/unused_ts/template-registry.js diff --git a/README.md b/README.md index bf08236..f8ce643 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,20 @@ $ netlify functions:create --name hello-world $ netlify functions:create hello-world --url https://github.com/netlify-labs/all-the-functions/tree/master/functions/9-using-middleware ``` +**Function Templates** + +Function templates can specify `addons` that they rely on as well as execute arbitrary code after installation in an `onComplete` hook, if a special `.netlify-function-template.js` file exists in the directory: + +```js +// .netlify-function-template.js +module.exports = { + addons: ['fauna'], + onComplete() { + console.log(`custom-template function created from template!`) + } +} +``` + #### Executing Netlify Functions After creating serverless functions, Netlify Dev can serve thes to you as part of your local build. This emulates the behaviour of Netlify Functions when deployed to Netlify. diff --git a/src/commands/functions/create.js b/src/commands/functions/create.js index 92ee525..3cf6311 100644 --- a/src/commands/functions/create.js +++ b/src/commands/functions/create.js @@ -4,7 +4,7 @@ const copy = require('copy-template-dir') const { flags } = require('@oclif/command') const Command = require('@netlify/cli-utils') const inquirer = require('inquirer') -const readRepoURL = require('../../utils/readRepoURL') +const { readRepoURL, validateRepoURL } = require('../../utils/readRepoURL') const { createSiteAddon } = require('../../utils/addons') const http = require('http') const fetch = require('node-fetch') @@ -111,11 +111,22 @@ async function pickTemplate() { // show separators return [ new inquirer.Separator(`----[JS]----`), - ...jsreg + ...jsreg, // new inquirer.Separator(`----[TS]----`), // ...tsreg, // new inquirer.Separator(`----[GO]----`), // ...goreg + new inquirer.Separator(`----[Special Commands]----`), + { + name: `*** Clone template from Github URL ***`, + value: 'url', + short: 'gh-url' + }, + { + name: `*** Report issue with, or suggest a new template ***`, + value: 'report', + short: 'gh-report' + } ] } else { // only show filtered results sorted by score @@ -144,7 +155,9 @@ async function pickTemplate() { }) } function formatRegistryArrayForInquirer(lang) { - const registry = require(path.join(templatesDir, lang, 'template-registry.js')) + const folderNames = fs.readdirSync(path.join(templatesDir, lang)) + const registry = folderNames + .map(name => require(path.join(templatesDir, lang, name, '.netlify-function-template.js'))) .sort((a, b) => (a.priority || 999) - (b.priority || 999)) .map(t => { t.lang = lang @@ -208,61 +221,103 @@ async function downloadFromURL(flags, args, functionsDir) { cp.exec('npm i', { cwd: path.join(functionsDir, nameToUse) }, () => { this.log(`installing dependencies for ${nameToUse} complete `) }) + + // read, execute, and delete function template file if exists + const fnTemplateFile = path.join(fnFolder, '.netlify-function-template.js') + if (fs.existsSync(fnTemplateFile)) { + const { onComplete, addons = [] } = require(fnTemplateFile) + installAddons.call(this, addons) + if (onComplete) onComplete() + fs.unlinkSync(fnTemplateFile) // delete + } } // no --url flag specified, pick from a provided template async function scaffoldFromTemplate(flags, args, functionsDir) { - const { onComplete, name: templateName, lang, addons = [] } = await pickTemplate() // pull the rest of the metadata from the template - const pathToTemplate = path.join(templatesDir, lang, templateName) - if (!fs.existsSync(pathToTemplate)) { - throw new Error(`there isnt a corresponding folder to the selected name, ${templateName} template is misconfigured`) - } - - const name = await getNameFromArgs(args, flags, templateName) - this.log(`Creating function ${name}`) - const functionPath = ensureFunctionPathIsOk(functionsDir, flags, name) - - // // SWYX: note to future devs - useful for debugging source to output issues - // this.log('from ', pathToTemplate, ' to ', functionPath) - const vars = { NETLIFY_STUFF_TO_REPLACE: 'REPLACEMENT' } // SWYX: TODO - let hasPackageJSON = false - copy(pathToTemplate, functionPath, vars, (err, createdFiles) => { - if (err) throw err - createdFiles.forEach(filePath => { - this.log(`Created ${filePath}`) - if (filePath.includes('package.json')) hasPackageJSON = true - }) - // rename functions with different names from default - if (name !== templateName) { - fs.renameSync(path.join(functionPath, templateName + '.js'), path.join(functionPath, name + '.js')) + const chosentemplate = await pickTemplate() // pull the rest of the metadata from the template + if (chosentemplate === 'url') { + const { chosenurl } = await inquirer.prompt([ + { + name: 'chosenurl', + message: 'URL to clone: ', + type: 'input', + validate: val => !!validateRepoURL(val) + // make sure it is not undefined and is a valid filename. + // this has some nuance i have ignored, eg crossenv and i18n concerns + } + ]) + flags.url = chosenurl.trim() + try { + await downloadFromURL.call(this, flags, args, functionsDir) + } catch (err) { + console.error('Error downloading from URL: ' + flags.url) + console.error(err) + process.exit(1) } - // npm install - if (hasPackageJSON) { - this.log(`installing dependencies for ${name}...`) - cp.exec('npm i', { cwd: path.join(functionPath) }, () => { - this.log(`installing dependencies for ${name} complete `) - }) + } else if (chosentemplate === 'report') { + console.log('opening in browser: https://github.com/netlify/netlify-dev-plugin/issues/new') + require('../../utils/openBrowser.js')('https://github.com/netlify/netlify-dev-plugin/issues/new') + } else { + const { onComplete, name: templateName, lang, addons = [] } = chosentemplate + + const pathToTemplate = path.join(templatesDir, lang, templateName) + if (!fs.existsSync(pathToTemplate)) { + throw new Error( + `there isnt a corresponding folder to the selected name, ${templateName} template is misconfigured` + ) } - if (addons.length) { - const { api, site } = this.netlify - const siteId = site.id - if (!siteId) { - this.log('No site id found, please run inside a site folder or `netlify link`') - return false + const name = await getNameFromArgs(args, flags, templateName) + this.log(`Creating function ${name}`) + const functionPath = ensureFunctionPathIsOk(functionsDir, flags, name) + + // // SWYX: note to future devs - useful for debugging source to output issues + // this.log('from ', pathToTemplate, ' to ', functionPath) + const vars = { NETLIFY_STUFF_TO_REPLACE: 'REPLACEMENT' } // SWYX: TODO + let hasPackageJSON = false + copy(pathToTemplate, functionPath, vars, (err, createdFiles) => { + if (err) throw err + createdFiles.forEach(filePath => { + this.log(`Created ${filePath}`) + if (filePath.includes('package.json')) hasPackageJSON = true + }) + // rename functions with different names from default + if (name !== templateName) { + fs.renameSync(path.join(functionPath, templateName + '.js'), path.join(functionPath, name + '.js')) } - api.getSite({ siteId }).then(async siteData => { - const accessToken = await this.authenticate() - const arr = addons.map(addonName => { - this.log('installing addon: ' + addonName) - // will prompt for configs if not supplied - we do not yet allow for addon configs supplied by `netlify functions:create` command and may never do so - return createSiteAddon(accessToken, addonName, siteId, siteData, log) + // delete function template file + fs.unlinkSync(path.join(functionPath, '.netlify-function-template.js')) + // npm install + if (hasPackageJSON) { + this.log(`installing dependencies for ${name}...`) + cp.exec('npm i', { cwd: path.join(functionPath) }, () => { + this.log(`installing dependencies for ${name} complete `) }) - return Promise.all(arr) - }) + } + installAddons.call(this, addons) + if (onComplete) onComplete() // do whatever the template wants to do after it is scaffolded + }) + } +} + +async function installAddons(addons = []) { + if (addons.length) { + const { api, site } = this.netlify + const siteId = site.id + if (!siteId) { + this.log('No site id found, please run inside a site folder or `netlify link`') + return false } - if (onComplete) onComplete() // do whatever the template wants to do after it is scaffolded - }) + return api.getSite({ siteId }).then(async siteData => { + const accessToken = await this.authenticate() + const arr = addons.map(addonName => { + this.log('installing addon: ' + addonName) + // will prompt for configs if not supplied - we do not yet allow for addon configs supplied by `netlify functions:create` command and may never do so + return createSiteAddon(accessToken, addonName, siteId, siteData, this.log) + }) + return Promise.all(arr) + }) + } } // we used to allow for a --dir command, diff --git a/src/functions-templates/js/apollo-graphql/.netlify-function-template.js b/src/functions-templates/js/apollo-graphql/.netlify-function-template.js new file mode 100644 index 0000000..1f9ad96 --- /dev/null +++ b/src/functions-templates/js/apollo-graphql/.netlify-function-template.js @@ -0,0 +1,7 @@ +module.exports = { + name: 'apollo-graphql', + description: 'GraphQL function using Apollo-Server-Lambda!', + onComplete() { + console.log(`apollo-graphql function created from template!`) + } +} diff --git a/src/functions-templates/js/auth-fetch/.netlify-function-template.js b/src/functions-templates/js/auth-fetch/.netlify-function-template.js new file mode 100644 index 0000000..190280c --- /dev/null +++ b/src/functions-templates/js/auth-fetch/.netlify-function-template.js @@ -0,0 +1,10 @@ +module.exports = { + name: 'auth-fetch', + description: 'Use `node-fetch` library and Netlify Identity to access APIs', + onComplete() { + console.log(`auth-fetch function created from template!`) + console.log( + 'REMINDER: Make sure to call this function with the Netlify Identity JWT. See https://netlify-gotrue-in-react.netlify.com/ for demo' + ) + } +} diff --git a/src/functions-templates/js/fauna-crud/.netlify-function-template.js b/src/functions-templates/js/fauna-crud/.netlify-function-template.js new file mode 100644 index 0000000..913ba11 --- /dev/null +++ b/src/functions-templates/js/fauna-crud/.netlify-function-template.js @@ -0,0 +1,8 @@ +module.exports = { + name: 'fauna-crud', + description: 'CRUD function using Fauna DB', + addons: ['fauna'], // in future we'll want to pass/prompt args to addons + onComplete() { + console.log(`fauna-crud function created from template!`) + } +} diff --git a/src/functions-templates/js/hello-world/.netlify-function-template.js b/src/functions-templates/js/hello-world/.netlify-function-template.js new file mode 100644 index 0000000..120aeff --- /dev/null +++ b/src/functions-templates/js/hello-world/.netlify-function-template.js @@ -0,0 +1,5 @@ +module.exports = { + name: 'hello-world', + priority: 1, + description: 'Basic function that shows async/await usage, and response formatting' +} diff --git a/src/functions-templates/js/node-fetch/.netlify-function-template.js b/src/functions-templates/js/node-fetch/.netlify-function-template.js new file mode 100644 index 0000000..65ce961 --- /dev/null +++ b/src/functions-templates/js/node-fetch/.netlify-function-template.js @@ -0,0 +1,7 @@ +module.exports = { + name: 'node-fetch', + description: 'Fetch function: uses node-fetch to hit an external API without CORS issues', + onComplete() { + console.log(`node-fetch function created from template!`) + } +} diff --git a/src/functions-templates/js/protected-function/.netlify-function-template.js b/src/functions-templates/js/protected-function/.netlify-function-template.js new file mode 100644 index 0000000..7cf90c2 --- /dev/null +++ b/src/functions-templates/js/protected-function/.netlify-function-template.js @@ -0,0 +1,7 @@ +module.exports = { + name: 'protected-function', + description: 'Function protected Netlify Identity authentication', + onComplete() { + console.log(`protected-function function created from template!`) + } +} diff --git a/src/functions-templates/js/serverless-ssr/.netlify-function-template.js b/src/functions-templates/js/serverless-ssr/.netlify-function-template.js new file mode 100644 index 0000000..1b15a4d --- /dev/null +++ b/src/functions-templates/js/serverless-ssr/.netlify-function-template.js @@ -0,0 +1,7 @@ +module.exports = { + name: 'serverless-ssr', + description: 'Dynamic serverside rendering via functions', + onComplete() { + console.log(`serverless-ssr function created from template!`) + } +} diff --git a/src/functions-templates/js/set-cookie/.netlify-function-template.js b/src/functions-templates/js/set-cookie/.netlify-function-template.js new file mode 100644 index 0000000..87f23e4 --- /dev/null +++ b/src/functions-templates/js/set-cookie/.netlify-function-template.js @@ -0,0 +1,7 @@ +module.exports = { + name: 'set-cookie', + description: 'Set a cookie alongside your function', + onComplete() { + console.log(`set-cookie function created from template!`) + } +} diff --git a/src/functions-templates/js/template-registry.js b/src/functions-templates/js/template-registry.js deleted file mode 100644 index 47f1525..0000000 --- a/src/functions-templates/js/template-registry.js +++ /dev/null @@ -1,72 +0,0 @@ -// every object should have: -// // a 'name' field that corresponds to a folder -// // "description" is just what shows in the CLI but we use the name as the identifier -// onComplete is optional. -// priority is optional - for controlling what shows first in CLI -module.exports = [ - { - name: 'auth-fetch', - description: 'Use `node-fetch` library and Netlify Identity to access APIs', - onComplete() { - console.log(`auth-fetch function created from template!`) - console.log( - 'REMINDER: Make sure to call this function with the Netlify Identity JWT. See https://netlify-gotrue-in-react.netlify.com/ for demo' - ) - } - }, - { - name: 'hello-world', - priority: 1, - description: 'Basic function that shows async/await usage, and response formatting' - }, - { - name: 'node-fetch', - description: 'Fetch function: uses node-fetch to hit an external API without CORS issues', - onComplete() { - console.log(`node-fetch function created from template!`) - } - }, - { - name: 'serverless-ssr', - description: 'Dynamic serverside rendering via functions', - onComplete() { - console.log(`serverless-ssr function created from template!`) - } - }, - { - name: 'set-cookie', - description: 'Set a cookie alongside your function', - onComplete() { - console.log(`set-cookie function created from template!`) - } - }, - { - name: 'protected-function', - description: 'Function protected Netlify Identity authentication', - onComplete() { - console.log(`protected-function function created from template!`) - } - }, - { - name: 'using-middleware', - description: 'Using Middleware with middy', - onComplete() { - console.log(`using-middleware function created from template!`) - } - }, - { - name: 'fauna-crud', - description: 'CRUD function using Fauna DB', - addons: ['fauna'], // in future we'll want to pass/prompt args to addons - onComplete() { - console.log(`fauna-crud function created from template!`) - } - }, - { - name: 'apollo-graphql', - description: 'GraphQL function using Apollo-Server-Lambda!', - onComplete() { - console.log(`apollo-graphql function created from template!`) - } - } -] diff --git a/src/functions-templates/js/using-middleware/.netlify-function-template.js b/src/functions-templates/js/using-middleware/.netlify-function-template.js new file mode 100644 index 0000000..78b99b0 --- /dev/null +++ b/src/functions-templates/js/using-middleware/.netlify-function-template.js @@ -0,0 +1,7 @@ +module.exports = { + name: 'using-middleware', + description: 'Using Middleware with middy', + onComplete() { + console.log(`using-middleware function created from template!`) + } +} diff --git a/src/functions-templates/unused_go/template-registry.js b/src/functions-templates/unused_go/template-registry.js deleted file mode 100644 index 8f4d528..0000000 --- a/src/functions-templates/unused_go/template-registry.js +++ /dev/null @@ -1,12 +0,0 @@ -// every object should have: -// // a 'name' field that corresponds to a folder -// // "description" is just what shows in the CLI but we use the name as the identifier -// onComplete is optional. -// priority is optional - for controlling what shows first in CLI -module.exports = [ - { - name: 'hello-world', - priority: 1, - description: 'Basic Hello World function in Golang' - } -] diff --git a/src/functions-templates/unused_ts/template-registry.js b/src/functions-templates/unused_ts/template-registry.js deleted file mode 100644 index 065dc8b..0000000 --- a/src/functions-templates/unused_ts/template-registry.js +++ /dev/null @@ -1,19 +0,0 @@ -// every object should have: -// // a 'name' field that corresponds to a folder -// // "description" is just what shows in the CLI but we use the name as the identifier -// onComplete is optional. -// priority is optional - for controlling what shows first in CLI -module.exports = [ - { - name: 'hello-world', - priority: 1, - description: 'Basic Hello World function: shows async/await usage, and response formatting' - }, - { - name: 'node-fetch', - description: 'Fetch function: uses node-fetch to hit an external API without CORS issues', - onComplete() { - console.log(`node-fetch function created from template!`) - } - } -] diff --git a/src/utils/readRepoURL.js b/src/utils/readRepoURL.js index 0bc312e..bc0abb9 100644 --- a/src/utils/readRepoURL.js +++ b/src/utils/readRepoURL.js @@ -13,7 +13,8 @@ const GITHUB = Symbol('GITHUB') */ async function readRepoURL(_url) { const URL = url.parse(_url) - const repoHost = validateRepoURL(URL) + const repoHost = validateRepoURL(_url) + if (repoHost !== GITHUB) throw new Error('only github repos are supported for now') const [owner_and_repo, contents_path] = parseRepoURL(repoHost, URL) const folderContents = await getRepoURLContents(repoHost, owner_and_repo, contents_path) return folderContents @@ -32,8 +33,9 @@ async function getRepoURLContents(repoHost, owner_and_repo, contents_path) { } } -function validateRepoURL(URL) { - if (URL.host !== 'github.com') throw new Error('only github repos are supported for now') +function validateRepoURL(_url) { + const URL = url.parse(_url) + if (URL.host !== 'github.com') return null // other validation logic here return GITHUB } @@ -48,4 +50,7 @@ function parseRepoURL(repoHost, URL) { } } -module.exports = readRepoURL +module.exports = { + readRepoURL, + validateRepoURL +}