diff --git a/package-lock.json b/package-lock.json index c9e78b1..cd835c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -164,13 +164,14 @@ "integrity": "sha512-9HpbxZX0LnU2IkaIYIjw6GCQZ6JMQ0Ai3F+9RrHr2jMAgXiiHRT19h2CiKEbjv44lYD5a0sRyV6kr+YRJVUQjg==" }, "@netlify/rules-proxy": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@netlify/rules-proxy/-/rules-proxy-0.1.2.tgz", - "integrity": "sha512-NiCF8EEYDAeqvaA6rwjyF/weFCQxYlztEuf9QIZmhTGLgEtvEs+0d8yD58mljdZ9df1HgMESwGyboSD6Kxohvw==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@netlify/rules-proxy/-/rules-proxy-0.1.3.tgz", + "integrity": "sha512-Q8d/nQWP6DdVo5VkHsXWWr5B77pcB1TIodEPa3gt2vAwpfslT/NTVP96y0+qwhlC2keb9eOGVOw1me5qjGaE7A==", "requires": { "chokidar": "^2.0.4", "cookie": "^0.3.1", "http-proxy-middleware": "^0.19.1", + "netlify-cli-logo": "^1.0.0", "netlify-redirect-parser": "^1.0.3", "netlify-redirector": "^0.0.4" } @@ -3004,13 +3005,13 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", - "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.8.tgz", + "integrity": "sha512-tPvHgPGB7m40CZ68xqFGkKuzN+RnpGmSV+hgeKxhRpbxdqKXUFJGC3yonBOLzQBcJyGpdZFDfCsdOC2KFsXzeA==", "optional": true, "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" }, "dependencies": { "abbrev": { @@ -3071,11 +3072,11 @@ "optional": true }, "debug": { - "version": "2.6.9", + "version": "4.1.1", "bundled": true, "optional": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "deep-extend": { @@ -3220,22 +3221,22 @@ } }, "ms": { - "version": "2.0.0", + "version": "2.1.1", "bundled": true, "optional": true }, "needle": { - "version": "2.2.4", + "version": "2.3.0", "bundled": true, "optional": true, "requires": { - "debug": "^2.1.2", + "debug": "^4.1.0", "iconv-lite": "^0.4.4", "sax": "^1.2.4" } }, "node-pre-gyp": { - "version": "0.10.3", + "version": "0.12.0", "bundled": true, "optional": true, "requires": { @@ -3261,12 +3262,12 @@ } }, "npm-bundled": { - "version": "1.0.5", + "version": "1.0.6", "bundled": true, "optional": true }, "npm-packlist": { - "version": "1.2.0", + "version": "1.4.1", "bundled": true, "optional": true, "requires": { @@ -3385,7 +3386,7 @@ "optional": true }, "semver": { - "version": "5.6.0", + "version": "5.7.0", "bundled": true, "optional": true }, @@ -3895,9 +3896,9 @@ "dev": true }, "handlebars": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.1.tgz", - "integrity": "sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -4277,9 +4278,9 @@ } }, "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" }, "is-accessor-descriptor": { "version": "0.1.6", @@ -5278,6 +5279,14 @@ } } }, + "netlify-cli-logo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/netlify-cli-logo/-/netlify-cli-logo-1.0.0.tgz", + "integrity": "sha512-+lTytTFc1y6RRN1UCmYcbBn+km0C66q2Qjzbz8/vFfMyyY1LbLVWBThbVgv1KVUxZDeciY9G8oOoiRKcSKyovA==", + "requires": { + "chalk": "^2.4.2" + } + }, "netlify-redirect-parser": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/netlify-redirect-parser/-/netlify-redirect-parser-1.0.3.tgz", @@ -6755,9 +6764,9 @@ } }, "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "pascalcase": { "version": "0.1.1", @@ -7029,12 +7038,12 @@ "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" }, "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", "requires": { "forwarded": "~0.1.2", - "ipaddr.js": "1.8.0" + "ipaddr.js": "1.9.0" } }, "pseudomap": { @@ -7155,6 +7164,11 @@ "strict-uri-encode": "^1.0.0" } }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, "ramda": { "version": "0.26.1", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", diff --git a/package.json b/package.json index 2341b98..ce6cc8a 100644 --- a/package.json +++ b/package.json @@ -6,26 +6,31 @@ "bugs": "https://github.com/netlify/netlify-dev-plugin/issues", "dependencies": { "@netlify/cli-utils": "^1.0.1", - "@netlify/rules-proxy": "^0.1.2", + "@netlify/rules-proxy": "^0.1.3", "@netlify/zip-it-and-ship-it": "^0.1.3", "@oclif/command": "^1", "@oclif/config": "^1", "ascii-table": "0.0.9", + "body-parser": "^1.18.3", "boxen": "^3.0.0", "chalk": "^2.4.2", "chokidar": "^2.1.5", "copy-template-dir": "^1.4.0", "execa": "^1.0.0", + "express": "^4.16.4", + "express-logging": "^1.1.1", "fs-extra": "^7.0.1", "fuzzy": "^0.1.3", - "get-port": "^4.1.0", + "get-port": "^4.2.0", "gh-release-fetch": "^1.0.3", "http-proxy": "^1.17.0", "inquirer": "^6.2.2", "inquirer-autocomplete-prompt": "^1.0.1", "netlify": "2.4.1", + "netlify-cli-logo": "^1.0.0", "node-fetch": "^2.3.0", "ora": "^3.4.0", + "querystring": "^0.2.0", "safe-join": "^0.1.2", "static-server": "^2.2.1", "wait-port": "^0.2.2", diff --git a/src/cli-logo.js b/src/cli-logo.js deleted file mode 100644 index b1c1a07..0000000 --- a/src/cli-logo.js +++ /dev/null @@ -1,10 +0,0 @@ -const chalk = require("chalk"); - -module.exports = { - NETLIFYDEV: `${chalk.greenBright("◈")} ${chalk.rgb(40, 180, 170)( - "Netlify Dev" - )} ${chalk.greenBright("◈")}`, - NETLIFYDEVLOG: `${chalk.greenBright("◈")}`, - NETLIFYDEVWARN: `${chalk.yellowBright("◈")}`, - NETLIFYDEVERR: `${chalk.redBright("◈")}` -}; diff --git a/src/commands/dev/exec.js b/src/commands/dev/exec.js index c3e31e4..9c721c0 100644 --- a/src/commands/dev/exec.js +++ b/src/commands/dev/exec.js @@ -6,7 +6,7 @@ const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR -} = require("../../cli-logo"); +} = require("netlify-cli-logo"); class ExecCommand extends Command { async run() { diff --git a/src/commands/dev/index.js b/src/commands/dev/index.js index 2d67f0f..d5d1b2d 100644 --- a/src/commands/dev/index.js +++ b/src/commands/dev/index.js @@ -5,7 +5,7 @@ const httpProxy = require("http-proxy"); const waitPort = require("wait-port"); const getPort = require("get-port"); const chokidar = require("chokidar"); -const { serveFunctions } = require("@netlify/zip-it-and-ship-it"); +const { serveFunctions } = require("../../utils/serveFunctions"); const { serverSettings } = require("../../detect-server"); const { detectFunctionsBuilder } = require("../../detect-functions-builder"); const Command = require("@netlify/cli-utils"); @@ -17,7 +17,7 @@ const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR -} = require("../../cli-logo"); +} = require("netlify-cli-logo"); const boxen = require("boxen"); const { createTunnel, connectTunnel } = require("../../live-tunnel"); diff --git a/src/commands/functions/build.js b/src/commands/functions/build.js index 4429280..19f31b5 100644 --- a/src/commands/functions/build.js +++ b/src/commands/functions/build.js @@ -7,7 +7,7 @@ const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR -} = require("../../cli-logo"); +} = require("netlify-cli-logo"); class FunctionsBuildCommand extends Command { async run() { diff --git a/src/commands/functions/create.js b/src/commands/functions/create.js index 5b312a6..47ee74d 100644 --- a/src/commands/functions/create.js +++ b/src/commands/functions/create.js @@ -19,7 +19,7 @@ const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR -} = require("../../cli-logo"); +} = require("netlify-cli-logo"); const templatesDir = path.resolve(__dirname, "../../functions-templates"); diff --git a/src/commands/functions/serve.js b/src/commands/functions/serve.js index 2e2d3d0..7efaaaf 100644 --- a/src/commands/functions/serve.js +++ b/src/commands/functions/serve.js @@ -4,7 +4,7 @@ const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR -} = require("../../cli-logo"); +} = require("netlify-cli-logo"); class FunctionsServeCommand extends Command { async run() { diff --git a/src/commands/functions/update.js b/src/commands/functions/update.js index c2ff0d0..b900f51 100644 --- a/src/commands/functions/update.js +++ b/src/commands/functions/update.js @@ -5,7 +5,7 @@ const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR -} = require("../../cli-logo"); +} = require("netlify-cli-logo"); class FunctionsUpdateCommand extends Command { async run() { diff --git a/src/detect-server.js b/src/detect-server.js index 93555ef..90d3ca9 100644 --- a/src/detect-server.js +++ b/src/detect-server.js @@ -5,7 +5,7 @@ const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR -} = require("./cli-logo"); +} = require("netlify-cli-logo"); const inquirer = require("inquirer"); const fs = require("fs"); const detectors = fs diff --git a/src/detectors/utils/jsdetect.js b/src/detectors/utils/jsdetect.js index 74fe543..8dd69ac 100644 --- a/src/detectors/utils/jsdetect.js +++ b/src/detectors/utils/jsdetect.js @@ -7,7 +7,7 @@ const { existsSync, readFileSync } = require("fs"); let pkgJSON = null; let yarnExists = false; let warnedAboutEmptyScript = false; -const { NETLIFYDEVWARN } = require("../../cli-logo.js"); +const { NETLIFYDEVWARN } = require("netlify-cli-logo"); /** hold package.json in a singleton so we dont do expensive parsing repeatedly */ function getPkgJSON() { diff --git a/src/live-tunnel.js b/src/live-tunnel.js index 4af19bd..c850d49 100644 --- a/src/live-tunnel.js +++ b/src/live-tunnel.js @@ -5,7 +5,11 @@ const path = require("path"); const execa = require("execa"); const chalk = require("chalk"); const { fetchLatest, updateAvailable } = require("gh-release-fetch"); -const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR } = require("./cli-logo"); +const { + NETLIFYDEVLOG, + NETLIFYDEVWARN, + NETLIFYDEVERR +} = require("netlify-cli-logo"); async function createTunnel(siteId, netlifyApiToken, log) { await installTunnelClient(log); diff --git a/src/utils/dev.js b/src/utils/dev.js index fac2b5a..2657a2c 100644 --- a/src/utils/dev.js +++ b/src/utils/dev.js @@ -2,7 +2,11 @@ // bit of a hasty abstraction but recommended by oclif const { getAddons } = require("netlify/src/addons"); const chalk = require("chalk"); -const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR } = require("../cli-logo"); +const { + NETLIFYDEVLOG, + NETLIFYDEVWARN, + NETLIFYDEVERR +} = require("netlify-cli-logo"); /** * inject environment variables from netlify addons and buildbot * into your local dev process.env diff --git a/src/utils/finders.js b/src/utils/finders.js new file mode 100644 index 0000000..4ba90c2 --- /dev/null +++ b/src/utils/finders.js @@ -0,0 +1,189 @@ +const path = require("path"); +const fs = require("fs"); +const packList = require("npm-packlist"); +const precinct = require("precinct"); +const resolve = require("resolve"); +const readPkgUp = require("read-pkg-up"); +const requirePackageName = require("require-package-name"); +const alwaysIgnored = new Set(["aws-sdk"]); +const debug = require("debug")("@netlify/zip-it-and-ship-it:finders"); + +const ignoredExtensions = new Set([ + ".log", + ".lock", + ".html", + ".md", + ".map", + ".ts", + ".png", + ".jpeg", + ".jpg", + ".gif", + ".css", + ".patch" +]); + +function ignoreMissing(dependency, optional) { + return alwaysIgnored.has(dependency) || (optional && dependency in optional); +} + +function includeModuleFile(packageJson, moduleFilePath) { + if (packageJson.files) { + return true; + } + + return !ignoredExtensions.has(path.extname(moduleFilePath)); +} + +function getDependencies(filename, basedir) { + const servicePath = basedir; + + const filePaths = new Set(); + const modulePaths = new Set(); + const pkgs = {}; + + const modulesToProcess = []; + const localFilesToProcess = [filename]; + + function handle(name, basedir, optionalDependencies) { + const moduleName = requirePackageName(name.replace(/\\/, "/")); + + if (alwaysIgnored.has(moduleName)) { + return; + } + + try { + const pathToModule = resolve.sync(path.join(moduleName, "package.json"), { + basedir + }); + const pkg = readPkgUp.sync({ cwd: pathToModule }); + + if (pkg) { + modulesToProcess.push(pkg); + } + } catch (e) { + if (e.code === "MODULE_NOT_FOUND") { + if (ignoreMissing(moduleName, optionalDependencies)) { + debug(`WARNING missing optional dependency: ${moduleName}`); + return null; + } + try { + // this resolves the requested import also against any set up NODE_PATH extensions, etc. + const resolved = require.resolve(name); + localFilesToProcess.push(resolved); + return; + } catch (e) { + throw new Error(`Could not find "${moduleName}" module in file: ${filename.replace( + path.dirname(basedir), + "" + )}. + +Please ensure "${moduleName}" is installed in the project.`); + } + } + throw e; + } + } + + while (localFilesToProcess.length) { + const currentLocalFile = localFilesToProcess.pop(); + + if (filePaths.has(currentLocalFile)) { + continue; + } + + filePaths.add(currentLocalFile); + precinct + .paperwork(currentLocalFile, { includeCore: false }) + .forEach(dependency => { + if (dependency.indexOf(".") === 0) { + const abs = resolve.sync(dependency, { + basedir: path.dirname(currentLocalFile) + }); + localFilesToProcess.push(abs); + } else { + handle(dependency, servicePath); + } + }); + } + + while (modulesToProcess.length) { + const currentModule = modulesToProcess.pop(); + const currentModulePath = path.join(currentModule.path, ".."); + const packageJson = currentModule.pkg; + + if (modulePaths.has(currentModulePath)) { + continue; + } + modulePaths.add(currentModulePath); + pkgs[currentModulePath] = packageJson; + ["dependencies", "peerDependencies", "optionalDependencies"].forEach( + key => { + const dependencies = packageJson[key]; + + if (dependencies) { + Object.keys(dependencies).forEach(dependency => { + handle( + dependency, + currentModulePath, + packageJson.optionalDependencies + ); + }); + } + } + ); + } + + modulePaths.forEach(modulePath => { + const packageJson = pkgs[modulePath]; + let moduleFilePaths; + + moduleFilePaths = packList.sync({ path: modulePath }); + + moduleFilePaths.forEach(moduleFilePath => { + if (includeModuleFile(packageJson, moduleFilePath)) { + filePaths.add(path.join(modulePath, moduleFilePath)); + } + }); + }); + + // TODO: get rid of this + const sizes = {}; + filePaths.forEach(filepath => { + const stat = fs.lstatSync(filepath); + const ext = path.extname(filepath); + sizes[ext] = (sizes[ext] || 0) + stat.size; + }); + debug("Sizes per extension: ", sizes); + + return Array.from(filePaths); +} + +function findModuleDir(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; +} + +function findHandler(functionPath) { + if (fs.lstatSync(functionPath).isFile()) { + return functionPath; + } + + const handlerPath = path.join( + functionPath, + `${path.basename(functionPath)}.js` + ); + if (!fs.existsSync(handlerPath)) { + return; + } + return handlerPath; +} + +module.exports = { getDependencies, findModuleDir, findHandler }; diff --git a/src/utils/serveFunctions.js b/src/utils/serveFunctions.js new file mode 100644 index 0000000..9f41a4d --- /dev/null +++ b/src/utils/serveFunctions.js @@ -0,0 +1,215 @@ +const fs = require("fs"); +const express = require("express"); +const bodyParser = require("body-parser"); +const expressLogging = require("express-logging"); +const queryString = require("querystring"); +const path = require("path"); +const getPort = require("get-port"); +const chokidar = require("chokidar"); +const chalk = require("chalk"); +const { + NETLIFYDEVLOG, + NETLIFYDEVWARN, + NETLIFYDEVERR +} = require("netlify-cli-logo"); + +const { findModuleDir, findHandler } = require("./finders"); + +const defaultPort = 34567; + +function handleErr(err, response) { + response.statusCode = 500; + response.write( + `${NETLIFYDEVERR} Function invocation failed: ` + err.toString() + ); + response.end(); + console.log(`${NETLIFYDEVERR} Error during invocation: `, err); + return; +} + +function createCallback(response) { + return function callback(err, lambdaResponse) { + if (err) { + return handleErr(err, response); + } + + response.statusCode = lambdaResponse.statusCode; + for (const key in lambdaResponse.headers) { + response.setHeader(key, lambdaResponse.headers[key]); + } + response.write( + lambdaResponse.isBase64Encoded + ? Buffer.from(lambdaResponse.body, "base64") + : lambdaResponse.body + ); + response.end(); + }; +} + +function promiseCallback(promise, callback) { + if (!promise) return; + if (typeof promise.then !== "function") return; + if (typeof callback !== "function") return; + + promise.then( + function(data) { + callback(null, data); + }, + function(err) { + callback(err, null); + } + ); +} + +function getHandlerPath(functionPath) { + if (functionPath.match(/\.js$/)) { + return functionPath; + } + + return path.join(functionPath, `${path.basename(functionPath)}.js`); +} + +function createHandler(dir, options) { + const functions = {}; + 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) + }; + } else if (fs.lstatSync(functionPath).isDirectory()) { + functions[file] = { + functionPath: handlerPath, + moduleDir: findModuleDir(functionPath) + }; + } + }); + + Object.keys(functions).forEach(name => { + const fn = functions[name]; + const clearCache = () => { + const before = module.paths; + module.paths = [fn.moduleDir]; + delete require.cache[require.resolve(fn.functionPath)]; + module.paths = before; + }; + fn.watcher = chokidar.watch( + [fn.functionPath, path.join(fn.moduleDir, "package.json")], + { + ignored: /node_modules/ + } + ); + fn.watcher + .on("add", clearCache) + .on("change", clearCache) + .on("unlink", clearCache); + }); + + return function(request, response) { + // handle proxies without path re-writes (http-servr) + const cleanPath = request.path.replace(/^\/.netlify\/functions/, ""); + + const func = cleanPath.split("/").filter(function(e) { + return e; + })[0]; + if (!functions[func]) { + response.statusCode = 404; + response.end("Function not found..."); + return; + } + const { functionPath, moduleDir } = functions[func]; + let handler; + let before = module.paths; + try { + module.paths = [moduleDir]; + handler = require(functionPath); + module.paths = before; + } catch (err) { + module.paths = before; + handleErr(err, response); + return; + } + + const isBase64 = + request.body && + !(request.headers["content-type"] || "").match( + /text|application|multipart\/form-data/ + ); + const lambdaRequest = { + path: request.path, + httpMethod: request.method, + queryStringParameters: queryString.parse(request.url.split(/\?(.+)/)[1]), + headers: request.headers, + body: isBase64 + ? Buffer.from(request.body.toString(), "utf8").toString("base64") + : request.body, + isBase64Encoded: isBase64 + }; + + const callback = createCallback(response); + const promise = handler.handler(lambdaRequest, {}, callback); + promiseCallback(promise, callback); + }; +} + +async function serveFunctions(settings, options) { + options = options || {}; + const app = express(); + const dir = settings.functionsDir; + const port = await getPort({ + port: assignLoudly(settings.port, defaultPort) + }); + + app.use(bodyParser.raw({ limit: "6mb" })); + app.use(bodyParser.text({ limit: "6mb", type: "*/*" })); + app.use( + expressLogging(console, { + blacklist: ["/favicon.ico"] + }) + ); + + app.get("/favicon.ico", function(req, res) { + res.status(204).end(); + }); + app.all("*", createHandler(dir, options)); + + app.listen(port, function(err) { + if (err) { + console.error(`${NETLIFYDEVERR} Unable to start lambda server: `, err); + process.exit(1); + } + + // add newline because this often appears alongside the client devserver's output + console.log(`\n${NETLIFYDEVLOG} Lambda server is listening on ${port}`); + }); + + return Promise.resolve({ + port + }); +} + +module.exports = { serveFunctions }; + +// if first arg is undefined, use default, but tell user about it in case it is unintentional +function assignLoudly( + optionalValue, + fallbackValue, + tellUser = dV => + console.log(`${NETLIFYDEVLOG} No port specified, using defaultPort of `, dV) +) { + if (fallbackValue === undefined) throw new Error("must have a fallbackValue"); + if (fallbackValue !== optionalValue && optionalValue === undefined) { + tellUser(fallbackValue); + return fallbackValue; + } else { + return optionalValue; + } +}