Skip to content
This repository has been archived by the owner on Sep 12, 2019. It is now read-only.

Commit

Permalink
Merge pull request #26 from netlify/netlify-functions/templatesV2
Browse files Browse the repository at this point in the history
add v2 of templates with assets and no requiring of all the things
  • Loading branch information
swyxio authored Mar 16, 2019
2 parents 657e442 + 01df863 commit 6bfc6aa
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 26 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@oclif/command": "^1",
"@oclif/config": "^1",
"ascii-table": "0.0.9",
"fs-extra": "^7.0.1",
"get-port": "^4.1.0",
"http-proxy": "^1.17.0",
"inquirer": "^6.2.2",
Expand Down
44 changes: 26 additions & 18 deletions src/commands/functions/create.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const fs = require('fs')
const fs = require('fs-extra')
const path = require('path')
const { flags } = require('@oclif/command')
const Command = require('@netlify/cli-utils')
Expand All @@ -8,29 +8,30 @@ const templatesDir = path.resolve(__dirname, '../../functions-templates')
class FunctionsCreateCommand extends Command {
async run() {
const { flags, args } = this.parse(FunctionsCreateCommand)
const name = await getNameFromArgs(args)
const { config } = this.netlify
const templates = fs
.readdirSync(templatesDir)
.filter(x => path.extname(x) === '.js') // only js templates for now
let templates = fs.readdirSync(templatesDir).filter(x => path.extname(x) === '.js') // only js templates for now
templates = templates
.map(t => require(path.join(templatesDir, t)))
.sort((a, b) => (a.priority || 999) - (b.priority || 999)) // doesnt scale but will be ok for now
const { templatePath } = await inquirer.prompt([
{
name: 'templatePath',
message: 'pick a template',
type: 'list',
choices: templates.map(t => {
return require(path.join(templatesDir, t)).metadata
// ({ name: path.basename(t, '.js') })
})
choices: templates.map(t => t.metadata)
}
])
// pull the rest of the metadata from the template
const { onComplete, copyAssets, templateCode } = require(path.join(templatesDir, templatePath))

let template = fs
.readFileSync(path.join(templatesDir, `${templatePath}.js`))
.toString()
.split('// --- Netlify Template Below -- //')
if (template.length !== 2) throw new Error('template ' + templatePath + ' badly formatted')
template = '// scaffolded from `netlify functions:create` \n' + template[1]
let template
try {
template = templateCode() // we may pass in args in future to customize the template
} catch (err) {
console.error('an error occurred retrieving template code, please check ' + templatePath, err)
process.exit(0)
}
const name = await getNameFromArgs(args, path.basename(templatePath, '.js'))

this.log(`Creating function ${name}`)

Expand Down Expand Up @@ -70,8 +71,14 @@ class FunctionsCreateCommand extends Command {
}

fs.writeFileSync(functionPath, template)

const onComplete = require(path.join(templatesDir, templatePath)).onComplete
if (copyAssets) {
copyAssets.forEach(src =>
fs.copySync(path.join(templatesDir, 'assets', src), path.join(functionsDir, src), {
overwrite: false,
errorOnExist: false // went with this to make it idempotent, might change in future
})
) // copy assets if specified
}
if (onComplete) onComplete() // do whatever the template wants to do after it is scaffolded
}
}
Expand All @@ -98,13 +105,14 @@ FunctionsCreateCommand.flags = {
module.exports = FunctionsCreateCommand

// prompt for a name if name not supplied
async function getNameFromArgs(args) {
async function getNameFromArgs(args, defaultName) {
let { name } = args
if (!name) {
let responses = await inquirer.prompt([
{
name: 'name',
message: 'name your function: ',
default: defaultName,
type: 'input',
validate: val => !!val && /^[\w\-.]+$/i.test(val)
// make sure it is not undefined and is a valid filename.
Expand Down
4 changes: 3 additions & 1 deletion src/functions-templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ we dont colocate this inside `src/commands/functions` because oclif will think i

## providing metadata (and other functionality)

we split the file based on the `// --- Netlify Template Below -- //` string. everything below it is cloned as the template. everything above it can be required and run as a module for configuring the template. for now we simply export a `metadata` object that fits [`inquirer's choices spec`](https://www.npmjs.com/package/inquirer#question).
we split the file based on the `// --- Netlify Template Below -- //` string. everything below it is cloned as the template. everything above it can be required and run as a module for configuring the template. for now we simply export a `metadata` object that fits [`inquirer's choices spec`](https://www.npmjs.com/package/inquirer#question).

once the templating is done we can also call an `onComplete` hook to print a reminder or execute other logic - see `node-fetch.js` for an example.

you can optionally set a `priority` to pin display order.

in future we can think about other options we may want to offer.

## future dev thoughts
Expand Down
3 changes: 3 additions & 0 deletions src/functions-templates/assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
this folder contains other folders that will be copied alongside the template.

`/app/index.js` contains a plain HTML template that we return from serverless function
115 changes: 115 additions & 0 deletions src/functions-templates/assets/app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* Express App */
const express = require('express')
const cors = require('cors')
const morgan = require('morgan')
const bodyParser = require('body-parser')
const compression = require('compression')

/* My express App */
module.exports = function expressApp(functionName) {
const app = express()
const router = express.Router()

// gzip responses
router.use(compression())

// Set router base path for local dev
const routerBasePath = process.env.NODE_ENV === 'dev' ? `/${functionName}` : `/.netlify/functions/${functionName}/`

/* define routes */
router.get('/', (req, res) => {
const html = `
<html>
<head>
<style>
body {
padding: 30px;
}
</style>
</head>
<body>
<h1>Express via '${functionName}' ⊂◉‿◉つ</h1>
<p>I'm using Express running via a <a href='https://www.netlify.com/docs/functions/' target='_blank'>Netlify Function</a>.</p>
<p>Choose a route:</p>
<div>
<a href='/.netlify/functions/${functionName}/users'>View /users route</a>
</div>
<div>
<a href='/.netlify/functions/${functionName}/hello'>View /hello route</a>
</div>
<br/>
<br/>
<div>
<a href='/'>
Go back to demo homepage
</a>
</div>
<br/>
<br/>
<div>
<a href='https://github.com/DavidWells/netlify-functions-express' target='_blank'>
See the source code on github
</a>
</div>
</body>
</html>
`
res.send(html)
})

router.get('/users', (req, res) => {
res.json({
users: [
{
name: 'steve'
},
{
name: 'joe'
}
]
})
})

router.get('/hello/', function(req, res) {
res.send('hello world')
})

// Attach logger
app.use(morgan(customLogger))

// Setup routes
app.use(routerBasePath, router)

// Apply express middlewares
router.use(cors())
router.use(bodyParser.json())
router.use(bodyParser.urlencoded({ extended: true }))

return app
}

function customLogger(tokens, req, res) {
const log = [
tokens.method(req, res),
tokens.url(req, res),
tokens.status(req, res),
tokens.res(req, res, 'content-length'),
'-',
tokens['response-time'](req, res),
'ms'
].join(' ')

if (process.env.NODE_ENV !== 'dev') {
// Log only in AWS context to get back function logs
console.log(log)
}
return log
}
51 changes: 51 additions & 0 deletions src/functions-templates/auth-fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
exports.metadata = {
name: 'Authenticated Fetch: uses node-fetch and Netlify Identity to access APIs',
value: 'auth-fetch',
short: 'auth-fetch'
}
exports.onComplete = () => {
console.log(`authenticated node-fetch function created from template!`)
console.log('REMINDER: Make sure to install `node-fetch` if you dont have it.')
console.log(
'REMINDER: Make sure to call this function with the Netlify Identity JWT. See https://netlify-gotrue-in-react.netlify.com/ for demo'
)
}

exports.templateCode = () => {
return `
// for a full working demo of Netlify Identity + Functions, see https://netlify-gotrue-in-react.netlify.com/
const fetch = require('node-fetch')
exports.handler = async function(event, context) {
if (!context.clientContext && !context.clientContext.identity) {
return {
statusCode: 500,
body: JSON.stringify({
msg:
'No identity instance detected. Did you enable it?'
}) // Could be a custom message or object i.e. JSON.stringify(err)
};
}
const { identity, user } = context.clientContext;
try {
const response = await fetch('https://api.chucknorris.io/jokes/random');
if (!response.ok) {
// NOT res.status >= 200 && res.status < 300
return { statusCode: response.status, body: response.statusText };
}
const data = await response.json();
return {
statusCode: 200,
body: JSON.stringify({ identity, user, msg: data.value })
};
} catch (err) {
console.log(err); // output to netlify function log
return {
statusCode: 500,
body: JSON.stringify({ msg: err.message }) // Could be a custom message or object i.e. JSON.stringify(err)
};
}
}
`
}
12 changes: 7 additions & 5 deletions src/functions-templates/hello-world.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// --- Netlify Template Metadata -- //
exports.priority = 1
exports.metadata = {
name: 'Basic Hello World function: shows async/await usage, and proper formatting with statusCode and body',
name: 'Basic Hello World function: shows async/await usage, and response formatting',
value: 'hello-world',
short: 'hello-world'
}
// exports.onComplete = () => {} // optional
// --- Netlify Template Below -- //
exports.templateCode = () => {
return `
async function hello() {
return Promise.resolve('Hello, World')
}

exports.handler = async function(event, context) {
try {
const body = await hello()
Expand All @@ -18,3 +18,5 @@ exports.handler = async function(event, context) {
return { statusCode: 500, body: err.toString() }
}
}
`
}
7 changes: 5 additions & 2 deletions src/functions-templates/node-fetch.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// --- Netlify Template Metadata -- //
exports.metadata = {
name: 'Fetch function: uses node-fetch to hit an external API without CORS issues',
value: 'node-fetch',
Expand All @@ -8,7 +7,9 @@ exports.onComplete = () => {
console.log(`node-fetch function created from template!`)
console.log('REMINDER: make sure to install `node-fetch` if you dont have it.')
}
// --- Netlify Template Below -- //

exports.templateCode = () => {
return `
const fetch = require('node-fetch')
exports.handler = async function(event, context) {
try {
Expand All @@ -31,3 +32,5 @@ exports.handler = async function(event, context) {
}
}
}
`
}
27 changes: 27 additions & 0 deletions src/functions-templates/serverless-http.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
exports.metadata = {
name: 'Serverless HTTP: dynamic serverside rendering via functions',
value: 'serverless-http',
short: 'serverless-http'
}
exports.onComplete = () => {
console.log(`serverless-http function created from template!`)
console.log('REMINDER: Make sure to `npm install serverless-http express cors morgan body-parser compression`.')
}
exports.copyAssets = ['app/index.js']

exports.templateCode = () => {
return `
// for a full working demo check https://express-via-functions.netlify.com/.netlify/functions/serverless-http
const serverless = require('serverless-http')
const expressApp = require('./app')
// We need to define our function name for express routes to set the correct base path
const functionName = 'serverless-http'
// Initialize express app
const app = expressApp(functionName)
// Export lambda handler
exports.handler = serverless(app)
`
}

0 comments on commit 6bfc6aa

Please sign in to comment.