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

Commit 6bfc6aa

Browse files
authored
Merge pull request #26 from netlify/netlify-functions/templatesV2
add v2 of templates with assets and no requiring of all the things
2 parents 657e442 + 01df863 commit 6bfc6aa

File tree

9 files changed

+238
-26
lines changed

9 files changed

+238
-26
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"@oclif/command": "^1",
1111
"@oclif/config": "^1",
1212
"ascii-table": "0.0.9",
13+
"fs-extra": "^7.0.1",
1314
"get-port": "^4.1.0",
1415
"http-proxy": "^1.17.0",
1516
"inquirer": "^6.2.2",

src/commands/functions/create.js

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const fs = require('fs')
1+
const fs = require('fs-extra')
22
const path = require('path')
33
const { flags } = require('@oclif/command')
44
const Command = require('@netlify/cli-utils')
@@ -8,29 +8,30 @@ const templatesDir = path.resolve(__dirname, '../../functions-templates')
88
class FunctionsCreateCommand extends Command {
99
async run() {
1010
const { flags, args } = this.parse(FunctionsCreateCommand)
11-
const name = await getNameFromArgs(args)
1211
const { config } = this.netlify
13-
const templates = fs
14-
.readdirSync(templatesDir)
15-
.filter(x => path.extname(x) === '.js') // only js templates for now
12+
let templates = fs.readdirSync(templatesDir).filter(x => path.extname(x) === '.js') // only js templates for now
13+
templates = templates
14+
.map(t => require(path.join(templatesDir, t)))
15+
.sort((a, b) => (a.priority || 999) - (b.priority || 999)) // doesnt scale but will be ok for now
1616
const { templatePath } = await inquirer.prompt([
1717
{
1818
name: 'templatePath',
1919
message: 'pick a template',
2020
type: 'list',
21-
choices: templates.map(t => {
22-
return require(path.join(templatesDir, t)).metadata
23-
// ({ name: path.basename(t, '.js') })
24-
})
21+
choices: templates.map(t => t.metadata)
2522
}
2623
])
24+
// pull the rest of the metadata from the template
25+
const { onComplete, copyAssets, templateCode } = require(path.join(templatesDir, templatePath))
2726

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

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

@@ -70,8 +71,14 @@ class FunctionsCreateCommand extends Command {
7071
}
7172

7273
fs.writeFileSync(functionPath, template)
73-
74-
const onComplete = require(path.join(templatesDir, templatePath)).onComplete
74+
if (copyAssets) {
75+
copyAssets.forEach(src =>
76+
fs.copySync(path.join(templatesDir, 'assets', src), path.join(functionsDir, src), {
77+
overwrite: false,
78+
errorOnExist: false // went with this to make it idempotent, might change in future
79+
})
80+
) // copy assets if specified
81+
}
7582
if (onComplete) onComplete() // do whatever the template wants to do after it is scaffolded
7683
}
7784
}
@@ -98,13 +105,14 @@ FunctionsCreateCommand.flags = {
98105
module.exports = FunctionsCreateCommand
99106

100107
// prompt for a name if name not supplied
101-
async function getNameFromArgs(args) {
108+
async function getNameFromArgs(args, defaultName) {
102109
let { name } = args
103110
if (!name) {
104111
let responses = await inquirer.prompt([
105112
{
106113
name: 'name',
107114
message: 'name your function: ',
115+
default: defaultName,
108116
type: 'input',
109117
validate: val => !!val && /^[\w\-.]+$/i.test(val)
110118
// make sure it is not undefined and is a valid filename.

src/functions-templates/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ we dont colocate this inside `src/commands/functions` because oclif will think i
88

99
## providing metadata (and other functionality)
1010

11-
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).
11+
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).
1212

1313
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.
1414

15+
you can optionally set a `priority` to pin display order.
16+
1517
in future we can think about other options we may want to offer.
1618

1719
## future dev thoughts
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
this folder contains other folders that will be copied alongside the template.
2+
3+
`/app/index.js` contains a plain HTML template that we return from serverless function
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/* Express App */
2+
const express = require('express')
3+
const cors = require('cors')
4+
const morgan = require('morgan')
5+
const bodyParser = require('body-parser')
6+
const compression = require('compression')
7+
8+
/* My express App */
9+
module.exports = function expressApp(functionName) {
10+
const app = express()
11+
const router = express.Router()
12+
13+
// gzip responses
14+
router.use(compression())
15+
16+
// Set router base path for local dev
17+
const routerBasePath = process.env.NODE_ENV === 'dev' ? `/${functionName}` : `/.netlify/functions/${functionName}/`
18+
19+
/* define routes */
20+
router.get('/', (req, res) => {
21+
const html = `
22+
<html>
23+
<head>
24+
<style>
25+
body {
26+
padding: 30px;
27+
}
28+
</style>
29+
</head>
30+
<body>
31+
<h1>Express via '${functionName}' ⊂◉‿◉つ</h1>
32+
33+
<p>I'm using Express running via a <a href='https://www.netlify.com/docs/functions/' target='_blank'>Netlify Function</a>.</p>
34+
35+
<p>Choose a route:</p>
36+
37+
<div>
38+
<a href='/.netlify/functions/${functionName}/users'>View /users route</a>
39+
</div>
40+
41+
<div>
42+
<a href='/.netlify/functions/${functionName}/hello'>View /hello route</a>
43+
</div>
44+
45+
<br/>
46+
<br/>
47+
48+
<div>
49+
<a href='/'>
50+
Go back to demo homepage
51+
</a>
52+
</div>
53+
54+
<br/>
55+
<br/>
56+
57+
<div>
58+
<a href='https://github.com/DavidWells/netlify-functions-express' target='_blank'>
59+
See the source code on github
60+
</a>
61+
</div>
62+
</body>
63+
</html>
64+
`
65+
res.send(html)
66+
})
67+
68+
router.get('/users', (req, res) => {
69+
res.json({
70+
users: [
71+
{
72+
name: 'steve'
73+
},
74+
{
75+
name: 'joe'
76+
}
77+
]
78+
})
79+
})
80+
81+
router.get('/hello/', function(req, res) {
82+
res.send('hello world')
83+
})
84+
85+
// Attach logger
86+
app.use(morgan(customLogger))
87+
88+
// Setup routes
89+
app.use(routerBasePath, router)
90+
91+
// Apply express middlewares
92+
router.use(cors())
93+
router.use(bodyParser.json())
94+
router.use(bodyParser.urlencoded({ extended: true }))
95+
96+
return app
97+
}
98+
99+
function customLogger(tokens, req, res) {
100+
const log = [
101+
tokens.method(req, res),
102+
tokens.url(req, res),
103+
tokens.status(req, res),
104+
tokens.res(req, res, 'content-length'),
105+
'-',
106+
tokens['response-time'](req, res),
107+
'ms'
108+
].join(' ')
109+
110+
if (process.env.NODE_ENV !== 'dev') {
111+
// Log only in AWS context to get back function logs
112+
console.log(log)
113+
}
114+
return log
115+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
exports.metadata = {
2+
name: 'Authenticated Fetch: uses node-fetch and Netlify Identity to access APIs',
3+
value: 'auth-fetch',
4+
short: 'auth-fetch'
5+
}
6+
exports.onComplete = () => {
7+
console.log(`authenticated node-fetch function created from template!`)
8+
console.log('REMINDER: Make sure to install `node-fetch` if you dont have it.')
9+
console.log(
10+
'REMINDER: Make sure to call this function with the Netlify Identity JWT. See https://netlify-gotrue-in-react.netlify.com/ for demo'
11+
)
12+
}
13+
14+
exports.templateCode = () => {
15+
return `
16+
// for a full working demo of Netlify Identity + Functions, see https://netlify-gotrue-in-react.netlify.com/
17+
18+
const fetch = require('node-fetch')
19+
exports.handler = async function(event, context) {
20+
if (!context.clientContext && !context.clientContext.identity) {
21+
return {
22+
statusCode: 500,
23+
body: JSON.stringify({
24+
msg:
25+
'No identity instance detected. Did you enable it?'
26+
}) // Could be a custom message or object i.e. JSON.stringify(err)
27+
};
28+
}
29+
const { identity, user } = context.clientContext;
30+
try {
31+
const response = await fetch('https://api.chucknorris.io/jokes/random');
32+
if (!response.ok) {
33+
// NOT res.status >= 200 && res.status < 300
34+
return { statusCode: response.status, body: response.statusText };
35+
}
36+
const data = await response.json();
37+
38+
return {
39+
statusCode: 200,
40+
body: JSON.stringify({ identity, user, msg: data.value })
41+
};
42+
} catch (err) {
43+
console.log(err); // output to netlify function log
44+
return {
45+
statusCode: 500,
46+
body: JSON.stringify({ msg: err.message }) // Could be a custom message or object i.e. JSON.stringify(err)
47+
};
48+
}
49+
}
50+
`
51+
}
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
// --- Netlify Template Metadata -- //
1+
exports.priority = 1
22
exports.metadata = {
3-
name: 'Basic Hello World function: shows async/await usage, and proper formatting with statusCode and body',
3+
name: 'Basic Hello World function: shows async/await usage, and response formatting',
44
value: 'hello-world',
55
short: 'hello-world'
66
}
7-
// exports.onComplete = () => {} // optional
8-
// --- Netlify Template Below -- //
7+
exports.templateCode = () => {
8+
return `
99
async function hello() {
1010
return Promise.resolve('Hello, World')
1111
}
12-
12+
1313
exports.handler = async function(event, context) {
1414
try {
1515
const body = await hello()
@@ -18,3 +18,5 @@ exports.handler = async function(event, context) {
1818
return { statusCode: 500, body: err.toString() }
1919
}
2020
}
21+
`
22+
}

src/functions-templates/node-fetch.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// --- Netlify Template Metadata -- //
21
exports.metadata = {
32
name: 'Fetch function: uses node-fetch to hit an external API without CORS issues',
43
value: 'node-fetch',
@@ -8,7 +7,9 @@ exports.onComplete = () => {
87
console.log(`node-fetch function created from template!`)
98
console.log('REMINDER: make sure to install `node-fetch` if you dont have it.')
109
}
11-
// --- Netlify Template Below -- //
10+
11+
exports.templateCode = () => {
12+
return `
1213
const fetch = require('node-fetch')
1314
exports.handler = async function(event, context) {
1415
try {
@@ -31,3 +32,5 @@ exports.handler = async function(event, context) {
3132
}
3233
}
3334
}
35+
`
36+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
exports.metadata = {
2+
name: 'Serverless HTTP: dynamic serverside rendering via functions',
3+
value: 'serverless-http',
4+
short: 'serverless-http'
5+
}
6+
exports.onComplete = () => {
7+
console.log(`serverless-http function created from template!`)
8+
console.log('REMINDER: Make sure to `npm install serverless-http express cors morgan body-parser compression`.')
9+
}
10+
exports.copyAssets = ['app/index.js']
11+
12+
exports.templateCode = () => {
13+
return `
14+
// for a full working demo check https://express-via-functions.netlify.com/.netlify/functions/serverless-http
15+
const serverless = require('serverless-http')
16+
const expressApp = require('./app')
17+
18+
// We need to define our function name for express routes to set the correct base path
19+
const functionName = 'serverless-http'
20+
21+
// Initialize express app
22+
const app = expressApp(functionName)
23+
24+
// Export lambda handler
25+
exports.handler = serverless(app)
26+
`
27+
}

0 commit comments

Comments
 (0)