Skip to content

Commit

Permalink
feat: support running builder functions locally (#3507)
Browse files Browse the repository at this point in the history
  • Loading branch information
emilyzhang authored Oct 18, 2021
1 parent 3ec4a54 commit 5a07055
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 7 deletions.
3 changes: 2 additions & 1 deletion src/commands/functions/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ class FunctionsServeCommand extends Command {
siteUrl,
capabilities,
timeouts,
prefix: '/.netlify/functions/',
functionsPrefix: '/.netlify/functions/',
buildersPrefix: '/.netlify/builders/',
})
}
}
Expand Down
33 changes: 28 additions & 5 deletions src/lib/functions/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const buildClientContext = function (headers) {
const createHandler = function ({ functionsRegistry }) {
return async function handler(request, response) {
// handle proxies without path re-writes (http-servr)
const cleanPath = request.path.replace(/^\/.netlify\/functions/, '')
const cleanPath = request.path.replace(/^\/.netlify\/(functions|builders)/, '')
const functionName = cleanPath.split('/').find(Boolean)
const func = functionsRegistry.get(functionName)

Expand Down Expand Up @@ -108,18 +108,30 @@ const createHandler = function ({ functionsRegistry }) {
} else {
const { error, result } = await func.invoke(event, clientContext)

// check for existence of metadata if this is a builder function
if (/^\/.netlify\/(builders)/.test(request.path) && (!result.metadata || !result.metadata.builder_function)) {
response.status(400).send({
message:
'Function is not an on-demand builder. See https://ntl.fyi/create-builder for how to convert a function to a builder.',
})
response.end()
return
}

handleSynchronousFunction(error, result, response)
}
}
}

const getFunctionsServer = function ({ functionsRegistry, siteUrl, prefix }) {
const getFunctionsServer = function ({ functionsRegistry, siteUrl, functionsPrefix, buildersPrefix }) {
// performance optimization, load express on demand
// eslint-disable-next-line node/global-require
const express = require('express')
// eslint-disable-next-line node/global-require
const expressLogging = require('express-logging')
const app = express()
const functionHandler = createHandler({ functionsRegistry })

app.set('query parser', 'simple')

app.use(
Expand All @@ -140,12 +152,22 @@ const getFunctionsServer = function ({ functionsRegistry, siteUrl, prefix }) {
res.status(204).end()
})

app.all(`${prefix}*`, createHandler({ functionsRegistry }))
app.all(`${functionsPrefix}*`, functionHandler)
app.all(`${buildersPrefix}*`, functionHandler)

return app
}

const startFunctionsServer = async ({ config, settings, site, siteUrl, capabilities, timeouts, prefix = '' }) => {
const startFunctionsServer = async ({
config,
settings,
site,
siteUrl,
capabilities,
timeouts,
functionsPrefix = '',
buildersPrefix = '',
}) => {
const internalFunctionsDir = await getInternalFunctionsDir({ base: site.root })

// The order of the function directories matters. Leftmost directories take
Expand All @@ -165,7 +187,8 @@ const startFunctionsServer = async ({ config, settings, site, siteUrl, capabilit
const server = getFunctionsServer({
functionsRegistry,
siteUrl,
prefix,
functionsPrefix,
buildersPrefix,
})

await startWebServer({ server, settings })
Expand Down
2 changes: 1 addition & 1 deletion src/utils/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const isInternal = function (url) {
return url.startsWith('/.netlify/')
}
const isFunction = function (functionsPort, url) {
return functionsPort && url.match(/^\/.netlify\/functions\/.+/)
return functionsPort && url.match(/^\/.netlify\/(functions|builders)\/.+/)
}

const getAddonUrl = function (addonsUrls, req) {
Expand Down
47 changes: 47 additions & 0 deletions tests/command.dev.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ testMatrix.forEach(({ args }) => {
return {
statusCode: 200,
body: 'ping',
metadata: { builder_function: true },
}
},
})
Expand All @@ -167,6 +168,36 @@ testMatrix.forEach(({ args }) => {
await withDevServer({ cwd: builder.directory, args }, async (server) => {
const response = await got(`${server.url}/.netlify/functions/timeout`).text()
t.is(response, 'ping')
const builderResponse = await got(`${server.url}/.netlify/builders/timeout`).text()
t.is(builderResponse, 'ping')
})
})
})

test(testName('should fail when no metadata is set for builder function', args), async (t) => {
await withSiteBuilder('site-with-misconfigured-builder-function', async (builder) => {
builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
path: 'builder.js',
handler: async () => ({
statusCode: 200,
body: 'ping',
}),
})

await builder.buildAsync()

await withDevServer({ cwd: builder.directory, args }, async (server) => {
const response = await got(`${server.url}/.netlify/functions/builder`)
t.is(response.body, 'ping')
t.is(response.statusCode, 200)
const builderResponse = await got(`${server.url}/.netlify/builders/builder`, {
throwHttpErrors: false,
})
t.is(
builderResponse.body,
`{"message":"Function is not an on-demand builder. See https://ntl.fyi/create-builder for how to convert a function to a builder."}`,
)
t.is(builderResponse.statusCode, 400)
})
})
})
Expand All @@ -178,6 +209,7 @@ testMatrix.forEach(({ args }) => {
handler: async () => ({
statusCode: 200,
body: 'ping',
metadata: { builder_function: true },
}),
})

Expand All @@ -186,6 +218,8 @@ testMatrix.forEach(({ args }) => {
await withDevServer({ cwd: builder.directory, args }, async (server) => {
const response = await got(`${server.url}/.netlify/functions/echo`).text()
t.is(response, 'ping')
const builderResponse = await got(`${server.url}/.netlify/builders/echo`).text()
t.is(builderResponse, 'ping')
})
})
})
Expand All @@ -200,6 +234,7 @@ testMatrix.forEach(({ args }) => {
handler: async () => ({
statusCode: 200,
body: `${process.env.TEST}`,
metadata: { builder_function: true },
}),
})

Expand All @@ -208,6 +243,8 @@ testMatrix.forEach(({ args }) => {
await withDevServer({ cwd: builder.directory, args }, async (server) => {
const response = await got(`${server.url}/.netlify/functions/env`).text()
t.is(response, 'FROM_DEV_FILE')
const builderResponse = await got(`${server.url}/.netlify/builders/env`).text()
t.is(builderResponse, 'FROM_DEV_FILE')
})
})
})
Expand All @@ -219,6 +256,7 @@ testMatrix.forEach(({ args }) => {
handler: async () => ({
statusCode: 200,
body: `${process.env.TEST}`,
metadata: { builder_function: true },
}),
})

Expand All @@ -227,6 +265,8 @@ testMatrix.forEach(({ args }) => {
await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => {
const response = await got(`${server.url}/.netlify/functions/env`).text()
t.is(response, 'FROM_PROCESS_ENV')
const builderResponse = await got(`${server.url}/.netlify/builders/env`).text()
t.is(builderResponse, 'FROM_PROCESS_ENV')
})
})
})
Expand All @@ -242,6 +282,7 @@ testMatrix.forEach(({ args }) => {
handler: async () => ({
statusCode: 200,
body: `${process.env.TEST}`,
metadata: { builder_function: true },
}),
})

Expand All @@ -250,6 +291,8 @@ testMatrix.forEach(({ args }) => {
await withDevServer({ cwd: builder.directory, args }, async (server) => {
const response = await got(`${server.url}/.netlify/functions/env`).text()
t.is(response, 'FROM_CONFIG_FILE')
const builderResponse = await got(`${server.url}/.netlify/builders/env`).text()
t.is(builderResponse, 'FROM_CONFIG_FILE')
})
})
})
Expand Down Expand Up @@ -1818,6 +1861,7 @@ export const handler = async function () {
body: '',
headers: { 'single-value-header': 'custom-value' },
multiValueHeaders: { 'multi-value-header': ['custom-value1', 'custom-value2'] },
metadata: { builder_function: true },
}),
})
.buildAsync()
Expand All @@ -1826,6 +1870,9 @@ export const handler = async function () {
const response = await got(`${server.url}/.netlify/functions/custom-headers`)
t.is(response.headers['single-value-header'], 'custom-value')
t.is(response.headers['multi-value-header'], 'custom-value1, custom-value2')
const builderResponse = await got(`${server.url}/.netlify/builders/custom-headers`)
t.is(builderResponse.headers['single-value-header'], 'custom-value')
t.is(builderResponse.headers['multi-value-header'], 'custom-value1, custom-value2')
})
})
})
Expand Down

1 comment on commit 5a07055

@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: 357 MB

Please sign in to comment.