diff --git a/src/commands/functions/invoke.js b/src/commands/functions/invoke.js index 377f23b98f6..74af17ed5b0 100644 --- a/src/commands/functions/invoke.js +++ b/src/commands/functions/invoke.js @@ -47,7 +47,7 @@ class FunctionsInvokeCommand extends Command { ) const port = flags.port || DEFAULT_PORT - const functions = getFunctions(functionsDir) + const functions = await getFunctions(functionsDir) const functionToTrigger = await getNameFromArgs(functions, args, flags) let headers = {} @@ -180,7 +180,7 @@ const processPayloadFromFlag = function (payloadString) { // also used in functions:create const getNameFromArgs = async function (functions, args, flags) { const functionToTrigger = getFunctionToTrigger(args, flags) - const functionNames = Object.keys(functions) + const functionNames = functions.map(getFunctionName) if (functionToTrigger) { if (functionNames.includes(functionToTrigger)) { @@ -193,6 +193,7 @@ const getNameFromArgs = async function (functions, args, flags) { )} supplied but no matching function found in your functions folder, forcing you to pick a valid one...`, ) } + const { trigger } = await inquirer.prompt([ { type: 'list', @@ -217,6 +218,10 @@ const getFunctionToTrigger = function (args, flags) { return args.name } +const getFunctionName = function ({ name }) { + return name +} + FunctionsInvokeCommand.description = `Trigger a function while in netlify dev with simulated data, good for testing function calls including Netlify's Event Triggered Functions` FunctionsInvokeCommand.aliases = ['function:trigger'] diff --git a/src/commands/functions/list.js b/src/commands/functions/list.js index c4eaa915f7a..3ee2910f29c 100644 --- a/src/commands/functions/list.js +++ b/src/commands/functions/list.js @@ -58,40 +58,35 @@ class FunctionsListCommand extends Command { process.exit(1) } - const functions = getFunctions(functionsDir) - const functionData = Object.entries(functions) + const functions = await getFunctions(functionsDir) + const normalizedFunctions = functions.map(normalizeFunction.bind(null, deployedFunctions)) - if (functionData.length === 0) { + if (normalizedFunctions.length === 0) { this.log(`No functions found in ${functionsDir}`) this.exit() } if (flags.json) { - const jsonData = functionData.map(([functionName, { moduleDir }]) => { - const isDeployed = deployedFunctions.map((deployedFunction) => deployedFunction.n).includes(functionName) - return { - name: functionName, - url: `/.netlify/functions/${functionName}`, - moduleDir, - isDeployed, - } - }) - this.logJson(jsonData) + this.logJson(normalizedFunctions) this.exit() } // Make table this.log(`Based on local functions folder ${functionsDir}, these are the functions detected`) const table = new AsciiTable(`Netlify Functions (in local functions folder)`) - table.setHeading('Name', 'Url', 'moduleDir', 'deployed') - functionData.forEach(([functionName, { moduleDir }]) => { - const isDeployed = deployedFunctions.map((deployedFunction) => deployedFunction.n).includes(functionName) - table.addRow(functionName, `/.netlify/functions/${functionName}`, moduleDir, isDeployed ? 'yes' : 'no') + table.setHeading('Name', 'URL', 'deployed') + normalizedFunctions.forEach(({ name, url, isDeployed }) => { + table.addRow(name, url, isDeployed ? 'yes' : 'no') }) this.log(table.toString()) } } +const normalizeFunction = function (deployedFunctions, { name, urlPath: url }) { + const isDeployed = deployedFunctions.some((deployedFunction) => deployedFunction.n === name) + return { name, url, isDeployed } +} + FunctionsListCommand.description = `List functions that exist locally Helpful for making sure that you have formatted your functions correctly diff --git a/src/utils/finders.js b/src/utils/finders.js deleted file mode 100644 index 81be0f1f1ae..00000000000 --- a/src/utils/finders.js +++ /dev/null @@ -1,32 +0,0 @@ -const fs = require('fs') -const path = require('path') - -const findModuleDir = function (dir) { - let basedir = dir - while (!fs.existsSync(path.join(basedir, 'package.json'))) { - const newBasedir = path.dirname(basedir) - if (newBasedir === basedir) { - return null - } - basedir = newBasedir - } - return basedir -} - -const findHandler = function (functionPath) { - if (fs.lstatSync(functionPath).isFile()) { - return functionPath - } - - const namedHandlerPath = path.join(functionPath, `${path.basename(functionPath)}.js`) - if (fs.existsSync(namedHandlerPath)) { - return namedHandlerPath - } - - const indexHandlerPath = path.join(functionPath, `index.js`) - if (fs.existsSync(indexHandlerPath)) { - return indexHandlerPath - } -} - -module.exports = { findModuleDir, findHandler } diff --git a/src/utils/get-functions.js b/src/utils/get-functions.js index 25f0e8f7d3a..4a5c643f6ec 100644 --- a/src/utils/get-functions.js +++ b/src/utils/get-functions.js @@ -1,43 +1,29 @@ -const fs = require('fs') -const path = require('path') +const { listFunctions } = require('@netlify/zip-it-and-ship-it') -const { findModuleDir, findHandler } = require('./finders') +const { fileExistsAsync } = require('../lib/fs') + +const getUrlPath = (functionName) => { + return `/.netlify/functions/${functionName}` +} const BACKGROUND = '-background' -const isBackground = (functionPath) => { - const filename = path.basename(functionPath, path.extname(functionPath)) - return filename.endsWith(BACKGROUND) +const addFunctionProps = ({ mainFile, name, runtime }) => { + const urlPath = getUrlPath(name) + const isBackground = name.endsWith(BACKGROUND) + return { mainFile, name, runtime, urlPath, isBackground } } -module.exports = { - getFunctions(dir) { - const functions = {} - if (fs.existsSync(dir)) { - fs.readdirSync(dir).forEach((file) => { - if (dir === 'node_modules') { - return - } - const functionPath = path.resolve(path.join(dir, file)) - const handlerPath = findHandler(functionPath) - if (!handlerPath) { - return - } - if (path.extname(functionPath) === '.js') { - functions[file.replace(/\.js$/, '')] = { - functionPath, - moduleDir: findModuleDir(functionPath), - isBackground: isBackground(functionPath), - } - } else if (fs.lstatSync(functionPath).isDirectory()) { - functions[file] = { - functionPath: handlerPath, - moduleDir: findModuleDir(functionPath), - isBackground: isBackground(handlerPath), - } - } - }) - } - return functions - }, +const JS = 'js' + +const getFunctions = async (functionsSrcDir) => { + if (!(await fileExistsAsync(functionsSrcDir))) { + return [] + } + + const functions = await listFunctions(functionsSrcDir) + const functionsWithProps = functions.filter(({ runtime }) => runtime === JS).map((func) => addFunctionProps(func)) + return functionsWithProps } + +module.exports = { getFunctions } diff --git a/src/utils/get-functions.test.js b/src/utils/get-functions.test.js index f2329df7b7b..6f50f28a4af 100644 --- a/src/utils/get-functions.test.js +++ b/src/utils/get-functions.test.js @@ -4,20 +4,19 @@ const test = require('ava') const { withSiteBuilder } = require('../../tests/utils/site-builder') -const { findModuleDir } = require('./finders') const { getFunctions } = require('./get-functions.js') -test('should return empty object when an empty string is provided', (t) => { - const funcs = getFunctions('') - t.deepEqual(funcs, {}) +test('should return empty object when an empty string is provided', async (t) => { + const funcs = await getFunctions('') + t.deepEqual(funcs, []) }) test('should return an empty object for a directory with no js files', async (t) => { await withSiteBuilder('site-without-functions', async (builder) => { await builder.buildAsync() - const funcs = getFunctions(builder.directory) - t.deepEqual(funcs, {}) + const funcs = await getFunctions(builder.directory) + t.deepEqual(funcs, []) }) }) @@ -30,14 +29,16 @@ test('should return object with function details for a directory with js files', await builder.buildAsync() const functions = path.join(builder.directory, 'functions') - const funcs = getFunctions(functions) - t.deepEqual(funcs, { - index: { - functionPath: path.join(functions, 'index.js'), - moduleDir: findModuleDir(functions), + const funcs = await getFunctions(functions) + t.deepEqual(funcs, [ + { + name: 'index', + mainFile: path.join(builder.directory, 'functions', 'index.js'), isBackground: false, + runtime: 'js', + urlPath: '/.netlify/functions/index', }, - }) + ]) }) }) @@ -55,18 +56,22 @@ test('should mark background functions based on filenames', async (t) => { await builder.buildAsync() const functions = path.join(builder.directory, 'functions') - const funcs = getFunctions(functions) - t.deepEqual(funcs, { - 'foo-background': { - functionPath: path.join(functions, 'foo-background.js'), - moduleDir: findModuleDir(functions), + const funcs = await getFunctions(functions) + t.deepEqual(funcs, [ + { + name: 'bar-background', + mainFile: path.join(builder.directory, 'functions', 'bar-background', 'bar-background.js'), isBackground: true, + runtime: 'js', + urlPath: '/.netlify/functions/bar-background', }, - 'bar-background': { - functionPath: path.join(functions, 'bar-background/bar-background.js'), - moduleDir: findModuleDir(functions), + { + name: 'foo-background', + mainFile: path.join(builder.directory, 'functions', 'foo-background.js'), isBackground: true, + runtime: 'js', + urlPath: '/.netlify/functions/foo-background', }, - }) + ]) }) }) diff --git a/src/utils/serve-functions.js b/src/utils/serve-functions.js index af79d835f87..97f4c37f546 100644 --- a/src/utils/serve-functions.js +++ b/src/utils/serve-functions.js @@ -159,8 +159,8 @@ const shouldBase64Encode = function (contentType) { const BASE_64_MIME_REGEXP = /image|audio|video|application\/pdf|application\/zip|applicaton\/octet-stream/i -const createHandler = function (dir) { - const functions = getFunctions(dir) +const createHandler = async function (dir) { + const functions = await getFunctions(dir) const watcher = chokidar.watch(dir, { ignored: /node_modules/ }) watcher.on('change', clearCache('modified')).on('unlink', clearCache('deleted')) @@ -176,12 +176,13 @@ const createHandler = function (dir) { const cleanPath = request.path.replace(/^\/.netlify\/functions/, '') const functionName = cleanPath.split('/').find(Boolean) - if (!functions[functionName]) { + const func = functions.find(({ name }) => name === functionName) + if (func === undefined) { response.statusCode = 404 response.end('Function not found...') return } - const { functionPath: lambdaPath, isBackground } = functions[functionName] + const { mainFile: lambdaPath, isBackground } = func const isBase64Encoded = shouldBase64Encode(request.headers['content-type']) const body = request.get('content-length') ? request.body.toString(isBase64Encoded ? 'base64' : 'utf8') : undefined @@ -345,7 +346,7 @@ const createFormSubmissionHandler = function (siteInfo) { } } -const serveFunctions = function (dir, siteInfo = {}) { +const serveFunctions = async function (dir, siteInfo = {}) { const app = express() app.set('query parser', 'simple') @@ -367,7 +368,7 @@ const serveFunctions = function (dir, siteInfo = {}) { res.status(204).end() }) - app.all('*', createHandler(dir)) + app.all('*', await createHandler(dir)) return app }