From 7bb16dccd237e74d632aa890a29e34783615f2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Wed, 7 Apr 2021 16:44:37 +0100 Subject: [PATCH 1/7] feat: serve bundled functions in ntl dev --- package-lock.json | 46 +++++++++++++--- package.json | 2 +- src/commands/dev/index.js | 1 + src/utils/bundle-functions.js | 101 ++++++++++++++++++++++++++++++++++ src/utils/serve-functions.js | 74 ++++++++++++++++--------- 5 files changed, 188 insertions(+), 36 deletions(-) create mode 100644 src/utils/bundle-functions.js diff --git a/package-lock.json b/package-lock.json index fa08adcc211..cde753c13ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@netlify/plugin-edge-handlers": "^1.11.6", "@netlify/plugins-list": "^2.6.0", "@netlify/traffic-mesh-agent": "^0.27.10", - "@netlify/zip-it-and-ship-it": "^3.1.0", + "@netlify/zip-it-and-ship-it": "^3.2.0", "@oclif/command": "^1.6.1", "@oclif/config": "^1.15.1", "@oclif/errors": "^1.3.4", @@ -2899,6 +2899,11 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@netlify/traffic-mesh-agent/-/traffic-mesh-agent-0.27.10.tgz", "integrity": "sha512-HZXEdIXzg8CpysYRDVXkBpmjOj/C8Zb8Q/qkkt9x+npJ56HeX6sXAE4vK4SMCRLkkbQ2VyYTaDKg++GefeB2Gg==", + "dependencies": { + "@netlify/traffic-mesh-agent-darwin-x64": "^0.27.10", + "@netlify/traffic-mesh-agent-linux-x64": "^0.27.10", + "@netlify/traffic-mesh-agent-win32-x64": "^0.27.10" + }, "optionalDependencies": { "@netlify/traffic-mesh-agent-darwin-x64": "^0.27.10", "@netlify/traffic-mesh-agent-linux-x64": "^0.27.10", @@ -2942,14 +2947,15 @@ ] }, "node_modules/@netlify/zip-it-and-ship-it": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@netlify/zip-it-and-ship-it/-/zip-it-and-ship-it-3.1.0.tgz", - "integrity": "sha512-x8Qz2ufrpz+dfv/HY4s/Xiwux4B0A4KbXp+vDdbTU8TkrIIRD9LGdzuwlCGBhTCufAWim3wARhSIZBC6+0k69w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@netlify/zip-it-and-ship-it/-/zip-it-and-ship-it-3.2.0.tgz", + "integrity": "sha512-4nWekXDaBsOAL4qLDZmMF/cPgBApFZTUoN+67IL5wqy+BnyABUmYuUBEzGO5rWVj1IkOlwm9qJvZUfLlsrZPOg==", "dependencies": { "archiver": "^4.0.0", "array-flat-polyfill": "^1.0.1", "common-path-prefix": "^2.0.0", "cp-file": "^7.0.0", + "del": "^5.1.0", "elf-cam": "^0.1.1", "end-of-stream": "^1.4.4", "esbuild": "^0.9.0", @@ -3551,6 +3557,9 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, + "dependencies": { + "graceful-fs": "^4.1.6" + }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -3718,6 +3727,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dependencies": { + "graceful-fs": "^4.1.6" + }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -3994,6 +4006,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dependencies": { + "graceful-fs": "^4.1.6" + }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -6640,6 +6655,7 @@ "dependencies": { "anymatch": "~3.1.1", "braces": "~3.0.2", + "fsevents": "~2.3.1", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -7021,6 +7037,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dependencies": { + "graceful-fs": "^4.1.6" + }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -9767,7 +9786,8 @@ "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2", - "optionator": "^0.8.1" + "optionator": "^0.8.1", + "source-map": "~0.6.1" }, "bin": { "escodegen": "bin/escodegen.js", @@ -12751,6 +12771,7 @@ "minimist": "^1.2.5", "neo-async": "^2.6.0", "source-map": "^0.6.1", + "uglify-js": "^3.1.4", "wordwrap": "^1.0.0" }, "bin": { @@ -14519,6 +14540,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -18726,6 +18748,9 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, + "dependencies": { + "graceful-fs": "^4.1.6" + }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -19526,6 +19551,9 @@ "version": "2.44.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.44.0.tgz", "integrity": "sha512-rGSF4pLwvuaH/x4nAS+zP6UNn5YUDWf/TeEU5IoXSZKBbKRNTCI3qMnYXKZgrC0D2KzS2baiOZt1OlqhMu5rnQ==", + "dependencies": { + "fsevents": "~2.3.1" + }, "bin": { "rollup": "dist/bin/rollup" }, @@ -24970,14 +24998,15 @@ "optional": true }, "@netlify/zip-it-and-ship-it": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@netlify/zip-it-and-ship-it/-/zip-it-and-ship-it-3.1.0.tgz", - "integrity": "sha512-x8Qz2ufrpz+dfv/HY4s/Xiwux4B0A4KbXp+vDdbTU8TkrIIRD9LGdzuwlCGBhTCufAWim3wARhSIZBC6+0k69w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@netlify/zip-it-and-ship-it/-/zip-it-and-ship-it-3.2.0.tgz", + "integrity": "sha512-4nWekXDaBsOAL4qLDZmMF/cPgBApFZTUoN+67IL5wqy+BnyABUmYuUBEzGO5rWVj1IkOlwm9qJvZUfLlsrZPOg==", "requires": { "archiver": "^4.0.0", "array-flat-polyfill": "^1.0.1", "common-path-prefix": "^2.0.0", "cp-file": "^7.0.0", + "del": "^5.1.0", "elf-cam": "^0.1.1", "end-of-stream": "^1.4.4", "esbuild": "^0.9.0", @@ -25184,6 +25213,7 @@ "resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.8.0.tgz", "integrity": "sha512-5vwpq6kbvwkQwKqAoOU3L72GZ3Ta8RRrewKj9OJRolx28KLJJ8Dg9Rf7obRwt5jQA9bkYd8gqzMTrI7H3xLfaw==", "requires": { + "@oclif/config": "^1.15.1", "@oclif/errors": "^1.3.3", "@oclif/parser": "^3.8.3", "@oclif/plugin-help": "^3", diff --git a/package.json b/package.json index 92c1cc3da99..e2b26743e83 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "@netlify/plugin-edge-handlers": "^1.11.6", "@netlify/plugins-list": "^2.6.0", "@netlify/traffic-mesh-agent": "^0.27.10", - "@netlify/zip-it-and-ship-it": "^3.1.0", + "@netlify/zip-it-and-ship-it": "^3.2.0", "@oclif/command": "^1.6.1", "@oclif/config": "^1.15.1", "@oclif/errors": "^1.3.4", diff --git a/src/commands/dev/index.js b/src/commands/dev/index.js index d1870dd63a1..3350aa4877c 100644 --- a/src/commands/dev/index.js +++ b/src/commands/dev/index.js @@ -256,6 +256,7 @@ class DevCommand extends Command { errorExit, siteUrl, capabilities, + functionsConfig: config.functions, }) await startFrameworkServer({ settings, log, exit }) diff --git a/src/utils/bundle-functions.js b/src/utils/bundle-functions.js new file mode 100644 index 00000000000..c2a25b0748c --- /dev/null +++ b/src/utils/bundle-functions.js @@ -0,0 +1,101 @@ +const { relative } = require('path') +const process = require('process') + +const { zipFunction, zipFunctions } = require('@netlify/zip-it-and-ship-it') +const chokidar = require('chokidar') +const debounce = require('lodash/debounce') +const makeDir = require('make-dir') + +const { getPathInProject } = require('../lib/settings') + +const { NETLIFYDEVERR, NETLIFYDEVLOG, NETLIFYDEVWARN } = require('./logo') + +const getOnChangeFunction = ({ functionsConfig, functionsDirectory, log, targetDirectory }) => async (path, action) => { + const relativePath = relative(functionsDirectory, path) + + log(`${NETLIFYDEVWARN} Function ${relativePath} ${action}, loading...`) + + try { + await zipFunction(path, targetDirectory, { + archiveFormat: 'none', + functionsConfig, + }) + + log(`${NETLIFYDEVLOG} Function ${relativePath} loaded!`) + } catch (error) { + log(`${NETLIFYDEVERR} Function ${relativePath} has thrown an error: ${error.message}`) + } +} + +// The function configuration keys returned by @netlify/config are not an exact +// match to the properties that @netlify/zip-it-and-ship-it expects. We do that +// translation here. +const normalizeFunctionConfig = (functionConfig = {}) => ({ + externalNodeModules: functionConfig.external_node_modules, + ignoredNodeModules: functionConfig.ignored_node_modules, + nodeBundler: functionConfig.node_bundler, +}) + +const startFunctionBundler = async ({ functionsConfig = {}, functionsDirectory, log }) => { + const targetDirectory = getPathInProject(['functions-serve']) + const normalizedFunctionsConfig = Object.entries(functionsConfig).reduce( + (result, [pattern, functionConfig]) => ({ + ...result, + [pattern]: normalizeFunctionConfig(functionConfig), + }), + {}, + ) + + log(`${NETLIFYDEVWARN} Bundling functions`) + + try { + await makeDir(targetDirectory) + } catch (error) { + log(`${NETLIFYDEVERR} Could not create directory: ${targetDirectory}`) + + return + } + + try { + await zipFunctions(functionsDirectory, targetDirectory, { + archiveFormat: 'none', + config: normalizedFunctionsConfig, + }) + } catch (error) { + log(`${NETLIFYDEVERR} An error occurred during function bundling`) + + process.exit(1) + } + + const onChange = debounce( + getOnChangeFunction({ functionsConfig: normalizedFunctionsConfig, functionsDirectory, log, targetDirectory }), + 300, + ) + + startWatch({ + functionsDirectory, + log, + onChange, + targetDirectory, + }) + + return { + functionsDirectory: targetDirectory, + } +} + +const startWatch = ({ functionsDirectory, onChange }) => { + const functionWatcher = chokidar.watch(functionsDirectory) + + functionWatcher.on('ready', () => { + functionWatcher.on('add', (path) => { + onChange(path, 'added') + }) + functionWatcher.on('change', (path) => { + onChange(path, 'changed') + }) + // functionWatcher.on('unlink', debouncedBuild) + }) +} + +module.exports = { startFunctionBundler } diff --git a/src/utils/serve-functions.js b/src/utils/serve-functions.js index ac99f40a6db..438569f722a 100644 --- a/src/utils/serve-functions.js +++ b/src/utils/serve-functions.js @@ -18,6 +18,7 @@ const winston = require('winston') const { getLogMessage } = require('../lib/log') +const { startFunctionBundler } = require('./bundle-functions') const { detectFunctionsBuilder } = require('./detect-functions-builder') const { getFunctions } = require('./get-functions') const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR } = require('./logo') @@ -153,12 +154,10 @@ const buildClientContext = function (headers) { } } -const clearCache = (action) => (path) => { - console.log(`${NETLIFYDEVLOG} ${path} ${action}, reloading...`) +const clearCache = () => { Object.keys(require.cache).forEach((key) => { delete require.cache[key] }) - console.log(`${NETLIFYDEVLOG} ${path} ${action}, successfully reloaded!`) } const shouldBase64Encode = function (contentType) { @@ -177,7 +176,7 @@ const createHandler = async function ({ dir, capabilities, warn }) { const functions = await getFunctions(dir) validateFunctions({ functions, capabilities, warn }) const watcher = chokidar.watch(dir, { ignored: /node_modules/ }) - watcher.on('change', clearCache('modified')).on('unlink', clearCache('deleted')) + watcher.on('change', clearCache).on('unlink', clearCache) const logger = winston.createLogger({ levels: winston.config.npm.levels, @@ -419,30 +418,35 @@ const getBuildFunction = ({ functionBuilder, log }) => const setupFunctionsBuilder = async ({ site, log, warn }) => { const functionBuilder = await detectFunctionsBuilder(site.root) - if (functionBuilder) { - log( - `${NETLIFYDEVLOG} Function builder ${chalk.yellow( - functionBuilder.builderName, - )} detected: Running npm script ${chalk.yellow(functionBuilder.npmScript)}`, - ) - warn( - `${NETLIFYDEVWARN} This is a beta feature, please give us feedback on how to improve at https://github.com/netlify/cli/`, - ) - const debouncedBuild = debounce(getBuildFunction({ functionBuilder, log }), 300, { - leading: true, - trailing: true, - }) + if (!functionBuilder) { + return + } - await debouncedBuild() + log( + `${NETLIFYDEVLOG} Function builder ${chalk.yellow( + functionBuilder.builderName, + )} detected: Running npm script ${chalk.yellow(functionBuilder.npmScript)}`, + ) + warn( + `${NETLIFYDEVWARN} This is a beta feature, please give us feedback on how to improve at https://github.com/netlify/cli/`, + ) - const functionWatcher = chokidar.watch(functionBuilder.src) - functionWatcher.on('ready', () => { - functionWatcher.on('add', debouncedBuild) - functionWatcher.on('change', debouncedBuild) - functionWatcher.on('unlink', debouncedBuild) - }) - } + const debouncedBuild = debounce(getBuildFunction({ functionBuilder, log }), 300, { + leading: true, + trailing: true, + }) + + await debouncedBuild() + + const functionWatcher = chokidar.watch(functionBuilder.src) + functionWatcher.on('ready', () => { + functionWatcher.on('add', debouncedBuild) + functionWatcher.on('change', debouncedBuild) + functionWatcher.on('unlink', debouncedBuild) + }) + + return functionBuilder } const startServer = async ({ server, settings, log, errorExit }) => { @@ -458,12 +462,28 @@ const startServer = async ({ server, settings, log, errorExit }) => { }) } -const startFunctionsServer = async ({ settings, site, log, warn, errorExit, siteUrl, capabilities }) => { +const startFunctionsServer = async ({ + settings, + site, + log, + warn, + errorExit, + siteUrl, + capabilities, + functionsConfig, +}) => { // serve functions from zip-it-and-ship-it // env variables relies on `url`, careful moving this code if (settings.functions) { await setupFunctionsBuilder({ site, log, warn }) - const server = await getFunctionsServer({ dir: settings.functions, siteUrl, capabilities, warn }) + + const { functionsDirectory } = await startFunctionBundler({ + functionsConfig, + functionsDirectory: settings.functions, + log, + }) + const server = await getFunctionsServer({ dir: functionsDirectory, siteUrl, capabilities, warn }) + await startServer({ server, settings, log, errorExit }) } } From 835086043dbd6a50f9f35266040f924236859c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Thu, 8 Apr 2021 11:45:59 +0100 Subject: [PATCH 2/7] feat: use function builder API --- src/commands/dev/index.js | 2 +- src/function-builder-detectors/zisi.js | 67 ++++++++++++++++ src/utils/bundle-functions.js | 101 ------------------------- src/utils/detect-functions-builder.js | 4 +- src/utils/serve-functions.js | 47 +++++------- 5 files changed, 91 insertions(+), 130 deletions(-) create mode 100644 src/function-builder-detectors/zisi.js delete mode 100644 src/utils/bundle-functions.js diff --git a/src/commands/dev/index.js b/src/commands/dev/index.js index 3350aa4877c..2f7ffc82467 100644 --- a/src/commands/dev/index.js +++ b/src/commands/dev/index.js @@ -249,6 +249,7 @@ class DevCommand extends Command { } await startFunctionsServer({ + config, settings, site, log, @@ -256,7 +257,6 @@ class DevCommand extends Command { errorExit, siteUrl, capabilities, - functionsConfig: config.functions, }) await startFrameworkServer({ settings, log, exit }) diff --git a/src/function-builder-detectors/zisi.js b/src/function-builder-detectors/zisi.js new file mode 100644 index 00000000000..cf08e03b7d7 --- /dev/null +++ b/src/function-builder-detectors/zisi.js @@ -0,0 +1,67 @@ +const path = require('path') +const process = require('process') + +const { zipFunction, zipFunctions } = require('@netlify/zip-it-and-ship-it') +const makeDir = require('make-dir') + +const { getPathInProject } = require('../lib/settings') +const { NETLIFYDEVERR } = require('../utils/logo') + +const bundleFunctions = ({ config, sourceDirectory, targetDirectory, updatedPath }) => { + // If `updatedPath` is truthy, it means we're running the build command due + // to an update to a file. If that's the case, we run `zipFunction` to bundle + // that specific function only. + if (updatedPath) { + return zipFunction(updatedPath, targetDirectory, { + archiveFormat: 'none', + config, + }) + } + + return zipFunctions(sourceDirectory, targetDirectory, { + archiveFormat: 'none', + config, + }) +} + +// The function configuration keys returned by @netlify/config are not an exact +// match to the properties that @netlify/zip-it-and-ship-it expects. We do that +// translation here. +const normalizeFunctionsConfig = (functionsConfig = {}) => + Object.entries(functionsConfig).reduce( + (result, [pattern, config]) => ({ + ...result, + [pattern]: { + externalNodeModules: config.external_node_modules, + ignoredNodeModules: config.ignored_node_modules, + nodeBundler: config.node_bundler === 'esbuild' ? 'esbuild_zisi' : config.node_bundler, + }, + }), + {}, + ) + +const getTargetDirectory = async ({ log }) => { + const targetDirectory = path.resolve(getPathInProject(['functions-serve'])) + + try { + await makeDir(targetDirectory) + } catch (error) { + log(`${NETLIFYDEVERR} Could not create directory: ${targetDirectory}`) + + process.exit(1) + } + + return targetDirectory +} + +module.exports = async function handler({ config, functionsDirectory: sourceDirectory, log }) { + const targetDirectory = await getTargetDirectory({ log }) + const functionsConfig = normalizeFunctionsConfig(config.functions) + + return { + build: (updatedPath) => bundleFunctions({ config: functionsConfig, sourceDirectory, targetDirectory, updatedPath }), + builderName: 'zip-it-and-ship-it', + src: sourceDirectory, + target: targetDirectory, + } +} diff --git a/src/utils/bundle-functions.js b/src/utils/bundle-functions.js deleted file mode 100644 index c2a25b0748c..00000000000 --- a/src/utils/bundle-functions.js +++ /dev/null @@ -1,101 +0,0 @@ -const { relative } = require('path') -const process = require('process') - -const { zipFunction, zipFunctions } = require('@netlify/zip-it-and-ship-it') -const chokidar = require('chokidar') -const debounce = require('lodash/debounce') -const makeDir = require('make-dir') - -const { getPathInProject } = require('../lib/settings') - -const { NETLIFYDEVERR, NETLIFYDEVLOG, NETLIFYDEVWARN } = require('./logo') - -const getOnChangeFunction = ({ functionsConfig, functionsDirectory, log, targetDirectory }) => async (path, action) => { - const relativePath = relative(functionsDirectory, path) - - log(`${NETLIFYDEVWARN} Function ${relativePath} ${action}, loading...`) - - try { - await zipFunction(path, targetDirectory, { - archiveFormat: 'none', - functionsConfig, - }) - - log(`${NETLIFYDEVLOG} Function ${relativePath} loaded!`) - } catch (error) { - log(`${NETLIFYDEVERR} Function ${relativePath} has thrown an error: ${error.message}`) - } -} - -// The function configuration keys returned by @netlify/config are not an exact -// match to the properties that @netlify/zip-it-and-ship-it expects. We do that -// translation here. -const normalizeFunctionConfig = (functionConfig = {}) => ({ - externalNodeModules: functionConfig.external_node_modules, - ignoredNodeModules: functionConfig.ignored_node_modules, - nodeBundler: functionConfig.node_bundler, -}) - -const startFunctionBundler = async ({ functionsConfig = {}, functionsDirectory, log }) => { - const targetDirectory = getPathInProject(['functions-serve']) - const normalizedFunctionsConfig = Object.entries(functionsConfig).reduce( - (result, [pattern, functionConfig]) => ({ - ...result, - [pattern]: normalizeFunctionConfig(functionConfig), - }), - {}, - ) - - log(`${NETLIFYDEVWARN} Bundling functions`) - - try { - await makeDir(targetDirectory) - } catch (error) { - log(`${NETLIFYDEVERR} Could not create directory: ${targetDirectory}`) - - return - } - - try { - await zipFunctions(functionsDirectory, targetDirectory, { - archiveFormat: 'none', - config: normalizedFunctionsConfig, - }) - } catch (error) { - log(`${NETLIFYDEVERR} An error occurred during function bundling`) - - process.exit(1) - } - - const onChange = debounce( - getOnChangeFunction({ functionsConfig: normalizedFunctionsConfig, functionsDirectory, log, targetDirectory }), - 300, - ) - - startWatch({ - functionsDirectory, - log, - onChange, - targetDirectory, - }) - - return { - functionsDirectory: targetDirectory, - } -} - -const startWatch = ({ functionsDirectory, onChange }) => { - const functionWatcher = chokidar.watch(functionsDirectory) - - functionWatcher.on('ready', () => { - functionWatcher.on('add', (path) => { - onChange(path, 'added') - }) - functionWatcher.on('change', (path) => { - onChange(path, 'changed') - }) - // functionWatcher.on('unlink', debouncedBuild) - }) -} - -module.exports = { startFunctionBundler } diff --git a/src/utils/detect-functions-builder.js b/src/utils/detect-functions-builder.js index 084fb2ddb77..b839446b5cc 100644 --- a/src/utils/detect-functions-builder.js +++ b/src/utils/detect-functions-builder.js @@ -1,7 +1,7 @@ const fs = require('fs') const path = require('path') -const detectFunctionsBuilder = async function (projectDir) { +const detectFunctionsBuilder = async function (parameters) { const detectors = fs .readdirSync(path.join(__dirname, '..', 'function-builder-detectors')) // only accept .js detector files @@ -11,7 +11,7 @@ const detectFunctionsBuilder = async function (projectDir) { for (const detector of detectors) { // eslint-disable-next-line no-await-in-loop - const settings = await detector(projectDir) + const settings = await detector(parameters) if (settings) { return settings } diff --git a/src/utils/serve-functions.js b/src/utils/serve-functions.js index 438569f722a..8700994577f 100644 --- a/src/utils/serve-functions.js +++ b/src/utils/serve-functions.js @@ -18,7 +18,6 @@ const winston = require('winston') const { getLogMessage } = require('../lib/log') -const { startFunctionBundler } = require('./bundle-functions') const { detectFunctionsBuilder } = require('./detect-functions-builder') const { getFunctions } = require('./get-functions') const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR } = require('./logo') @@ -390,7 +389,7 @@ const getFunctionsServer = async function ({ dir, siteUrl, capabilities, warn }) } const getBuildFunction = ({ functionBuilder, log }) => - async function build() { + async function build(updatedPath) { log( `${NETLIFYDEVLOG} Function builder ${chalk.yellow(functionBuilder.builderName)} ${chalk.magenta( 'building', @@ -398,7 +397,7 @@ const getBuildFunction = ({ functionBuilder, log }) => ) try { - await functionBuilder.build() + await functionBuilder.build(updatedPath) log( `${NETLIFYDEVLOG} Function builder ${chalk.yellow(functionBuilder.builderName)} ${chalk.green( 'finished', @@ -416,18 +415,18 @@ const getBuildFunction = ({ functionBuilder, log }) => } } -const setupFunctionsBuilder = async ({ site, log, warn }) => { - const functionBuilder = await detectFunctionsBuilder(site.root) +const setupFunctionsBuilder = async ({ config, functionsDirectory, log, site, warn }) => { + const functionBuilder = await detectFunctionsBuilder({ config, functionsDirectory, log, projectRoot: site.root }) if (!functionBuilder) { - return + return {} } - log( - `${NETLIFYDEVLOG} Function builder ${chalk.yellow( - functionBuilder.builderName, - )} detected: Running npm script ${chalk.yellow(functionBuilder.npmScript)}`, - ) + const npmScriptString = functionBuilder.npmScript + ? `: Running npm script ${chalk.yellow(functionBuilder.npmScript)}` + : '' + + log(`${NETLIFYDEVLOG} Function builder ${chalk.yellow(functionBuilder.builderName)} detected${npmScriptString}.`) warn( `${NETLIFYDEVWARN} This is a beta feature, please give us feedback on how to improve at https://github.com/netlify/cli/`, ) @@ -462,27 +461,23 @@ const startServer = async ({ server, settings, log, errorExit }) => { }) } -const startFunctionsServer = async ({ - settings, - site, - log, - warn, - errorExit, - siteUrl, - capabilities, - functionsConfig, -}) => { +const startFunctionsServer = async ({ config, settings, site, log, warn, errorExit, siteUrl, capabilities }) => { // serve functions from zip-it-and-ship-it // env variables relies on `url`, careful moving this code if (settings.functions) { - await setupFunctionsBuilder({ site, log, warn }) - - const { functionsDirectory } = await startFunctionBundler({ - functionsConfig, + const { target: functionsDirectory } = await setupFunctionsBuilder({ + config, functionsDirectory: settings.functions, log, + site, + warn, + }) + const server = await getFunctionsServer({ + dir: functionsDirectory || settings.functions, + siteUrl, + capabilities, + warn, }) - const server = await getFunctionsServer({ dir: functionsDirectory, siteUrl, capabilities, warn }) await startServer({ server, settings, log, errorExit }) } From 26e27d172bb3ca45c41d5b9f8971073f46032ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Thu, 8 Apr 2021 12:38:20 +0100 Subject: [PATCH 3/7] feat: add omitFileChangesLog property --- src/function-builder-detectors/zisi.js | 1 + src/utils/serve-functions.js | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/function-builder-detectors/zisi.js b/src/function-builder-detectors/zisi.js index cf08e03b7d7..e8e9e4a3d67 100644 --- a/src/function-builder-detectors/zisi.js +++ b/src/function-builder-detectors/zisi.js @@ -61,6 +61,7 @@ module.exports = async function handler({ config, functionsDirectory: sourceDire return { build: (updatedPath) => bundleFunctions({ config: functionsConfig, sourceDirectory, targetDirectory, updatedPath }), builderName: 'zip-it-and-ship-it', + omitFileChangesLog: true, src: sourceDirectory, target: targetDirectory, } diff --git a/src/utils/serve-functions.js b/src/utils/serve-functions.js index 8700994577f..e398e33d6d5 100644 --- a/src/utils/serve-functions.js +++ b/src/utils/serve-functions.js @@ -153,10 +153,18 @@ const buildClientContext = function (headers) { } } -const clearCache = () => { +const clearCache = ({ action, omitLog }) => (path) => { + if (!omitLog) { + console.log(`${NETLIFYDEVLOG} ${path} ${action}, reloading...`) + } + Object.keys(require.cache).forEach((key) => { delete require.cache[key] }) + + if (!omitLog) { + console.log(`${NETLIFYDEVLOG} ${path} ${action}, successfully reloaded!`) + } } const shouldBase64Encode = function (contentType) { @@ -171,11 +179,13 @@ const validateFunctions = function ({ functions, capabilities, warn }) { } } -const createHandler = async function ({ dir, capabilities, warn }) { +const createHandler = async function ({ dir, capabilities, omitFileChangesLog, warn }) { const functions = await getFunctions(dir) validateFunctions({ functions, capabilities, warn }) const watcher = chokidar.watch(dir, { ignored: /node_modules/ }) - watcher.on('change', clearCache).on('unlink', clearCache) + watcher + .on('change', clearCache({ action: 'modified', omitLog: omitFileChangesLog })) + .on('unlink', clearCache({ action: 'deleted', omitLog: omitFileChangesLog })) const logger = winston.createLogger({ levels: winston.config.npm.levels, @@ -361,7 +371,7 @@ const createFormSubmissionHandler = function ({ siteUrl, warn }) { } } -const getFunctionsServer = async function ({ dir, siteUrl, capabilities, warn }) { +const getFunctionsServer = async function ({ dir, omitFileChangesLog, siteUrl, capabilities, warn }) { const app = express() app.set('query parser', 'simple') @@ -383,7 +393,7 @@ const getFunctionsServer = async function ({ dir, siteUrl, capabilities, warn }) res.status(204).end() }) - app.all('*', await createHandler({ dir, capabilities, warn })) + app.all('*', await createHandler({ dir, capabilities, omitFileChangesLog, warn })) return app } @@ -465,7 +475,7 @@ const startFunctionsServer = async ({ config, settings, site, log, warn, errorEx // serve functions from zip-it-and-ship-it // env variables relies on `url`, careful moving this code if (settings.functions) { - const { target: functionsDirectory } = await setupFunctionsBuilder({ + const { omitFileChangesLog, target: functionsDirectory } = await setupFunctionsBuilder({ config, functionsDirectory: settings.functions, log, @@ -474,6 +484,7 @@ const startFunctionsServer = async ({ config, settings, site, log, warn, errorEx }) const server = await getFunctionsServer({ dir: functionsDirectory || settings.functions, + omitFileChangesLog, siteUrl, capabilities, warn, From 78f4ec0c541d8d21d7c3f2d42596c08aff87ef95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Thu, 8 Apr 2021 12:53:00 +0100 Subject: [PATCH 4/7] fix: use errorExit instead of process.exit --- src/function-builder-detectors/zisi.js | 11 ++++------- src/utils/serve-functions.js | 11 +++++++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/function-builder-detectors/zisi.js b/src/function-builder-detectors/zisi.js index e8e9e4a3d67..cf17eb095cb 100644 --- a/src/function-builder-detectors/zisi.js +++ b/src/function-builder-detectors/zisi.js @@ -1,5 +1,4 @@ const path = require('path') -const process = require('process') const { zipFunction, zipFunctions } = require('@netlify/zip-it-and-ship-it') const makeDir = require('make-dir') @@ -40,22 +39,20 @@ const normalizeFunctionsConfig = (functionsConfig = {}) => {}, ) -const getTargetDirectory = async ({ log }) => { +const getTargetDirectory = async ({ errorExit }) => { const targetDirectory = path.resolve(getPathInProject(['functions-serve'])) try { await makeDir(targetDirectory) } catch (error) { - log(`${NETLIFYDEVERR} Could not create directory: ${targetDirectory}`) - - process.exit(1) + errorExit(`${NETLIFYDEVERR} Could not create directory: ${targetDirectory}`) } return targetDirectory } -module.exports = async function handler({ config, functionsDirectory: sourceDirectory, log }) { - const targetDirectory = await getTargetDirectory({ log }) +module.exports = async function handler({ config, errorExit, functionsDirectory: sourceDirectory }) { + const targetDirectory = await getTargetDirectory({ errorExit }) const functionsConfig = normalizeFunctionsConfig(config.functions) return { diff --git a/src/utils/serve-functions.js b/src/utils/serve-functions.js index e398e33d6d5..011fcf92014 100644 --- a/src/utils/serve-functions.js +++ b/src/utils/serve-functions.js @@ -425,8 +425,14 @@ const getBuildFunction = ({ functionBuilder, log }) => } } -const setupFunctionsBuilder = async ({ config, functionsDirectory, log, site, warn }) => { - const functionBuilder = await detectFunctionsBuilder({ config, functionsDirectory, log, projectRoot: site.root }) +const setupFunctionsBuilder = async ({ config, errorExit, functionsDirectory, log, site, warn }) => { + const functionBuilder = await detectFunctionsBuilder({ + config, + errorExit, + functionsDirectory, + log, + projectRoot: site.root, + }) if (!functionBuilder) { return {} @@ -477,6 +483,7 @@ const startFunctionsServer = async ({ config, settings, site, log, warn, errorEx if (settings.functions) { const { omitFileChangesLog, target: functionsDirectory } = await setupFunctionsBuilder({ config, + errorExit, functionsDirectory: settings.functions, log, site, From 1b2278b755c89a57965d5f7dc2d730d366079183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Thu, 8 Apr 2021 14:14:14 +0100 Subject: [PATCH 5/7] feat: use zisi builder only when esbuild is enabled --- src/function-builder-detectors/zisi.js | 8 +++++++- src/utils/detect-functions-builder.js | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/function-builder-detectors/zisi.js b/src/function-builder-detectors/zisi.js index cf17eb095cb..c3112dec247 100644 --- a/src/function-builder-detectors/zisi.js +++ b/src/function-builder-detectors/zisi.js @@ -52,8 +52,14 @@ const getTargetDirectory = async ({ errorExit }) => { } module.exports = async function handler({ config, errorExit, functionsDirectory: sourceDirectory }) { - const targetDirectory = await getTargetDirectory({ errorExit }) const functionsConfig = normalizeFunctionsConfig(config.functions) + const isUsingEsbuild = functionsConfig['*'] && functionsConfig['*'].nodeBundler === 'esbuild_zisi' + + if (!isUsingEsbuild) { + return false + } + + const targetDirectory = await getTargetDirectory({ errorExit }) return { build: (updatedPath) => bundleFunctions({ config: functionsConfig, sourceDirectory, targetDirectory, updatedPath }), diff --git a/src/utils/detect-functions-builder.js b/src/utils/detect-functions-builder.js index b839446b5cc..c3bf5054db3 100644 --- a/src/utils/detect-functions-builder.js +++ b/src/utils/detect-functions-builder.js @@ -6,6 +6,8 @@ const detectFunctionsBuilder = async function (parameters) { .readdirSync(path.join(__dirname, '..', 'function-builder-detectors')) // only accept .js detector files .filter((filename) => filename.endsWith('.js')) + // Sorting by filename + .sort() // eslint-disable-next-line node/global-require, import/no-dynamic-require .map((det) => require(path.join(__dirname, '..', `function-builder-detectors/${det}`))) From 88f1f47bf5e68589a8cecb626a464e89b1732884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Thu, 8 Apr 2021 14:14:44 +0100 Subject: [PATCH 6/7] chore: add function builder tests --- tests/command.dev.test.js | 82 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/command.dev.test.js b/tests/command.dev.test.js index 676bf9a9735..02334469bcd 100644 --- a/tests/command.dev.test.js +++ b/tests/command.dev.test.js @@ -1454,6 +1454,88 @@ testMatrix.forEach(({ args }) => { }) }) }) + + test(testName('Should not use the ZISI function bundler if not using esbuild', args), async (t) => { + await withSiteBuilder('site-with-esm-function', async (builder) => { + builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withContentFile({ + path: path.join('functions', 'esm-function', 'esm-function.js'), + content: ` +export async function handler(event, context) { + return { + statusCode: 200, + body: 'esm', + }; +} + `, + }) + + await builder.buildAsync() + + await t.throwsAsync(() => + withDevServer({ cwd: builder.directory, args }, async (server) => + got(`${server.url}/.netlify/functions/esm-function`).text(), + ), + ) + }) + }) + + test(testName('Should use the ZISI function bundler and serve ESM functions if using esbuild', args), async (t) => { + await withSiteBuilder('site-with-esm-function', async (builder) => { + builder + .withNetlifyToml({ config: { functions: { directory: 'functions', node_bundler: 'esbuild' } } }) + .withContentFile({ + path: path.join('functions', 'esm-function', 'esm-function.js'), + content: ` +export async function handler(event, context) { + return { + statusCode: 200, + body: 'esm', + }; +} + `, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/esm-function`).text() + t.is(response, 'esm') + }) + }) + }) + + test( + testName('Should use the ZISI function bundler and serve TypeScript functions if using esbuild', args), + async (t) => { + await withSiteBuilder('site-with-ts-function', async (builder) => { + builder + .withNetlifyToml({ config: { functions: { directory: 'functions', node_bundler: 'esbuild' } } }) + .withContentFile({ + path: path.join('functions', 'ts-function', 'ts-function.ts'), + content: ` +type CustomResponse = string; + +export const handler = async function () { + const response: CustomResponse = "ts"; + + return { + statusCode: 200, + body: response, + }; +}; + + `, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/ts-function`).text() + t.is(response, 'ts') + }) + }) + }, + ) } }) /* eslint-enable require-await */ From 4ad71a4960407fc2397ac485c07acb331245b490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Thu, 8 Apr 2021 15:19:42 +0100 Subject: [PATCH 7/7] chore: remove warning message --- src/utils/serve-functions.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/utils/serve-functions.js b/src/utils/serve-functions.js index 011fcf92014..64282c9bdf4 100644 --- a/src/utils/serve-functions.js +++ b/src/utils/serve-functions.js @@ -20,7 +20,7 @@ const { getLogMessage } = require('../lib/log') const { detectFunctionsBuilder } = require('./detect-functions-builder') const { getFunctions } = require('./get-functions') -const { NETLIFYDEVLOG, NETLIFYDEVWARN, NETLIFYDEVERR } = require('./logo') +const { NETLIFYDEVLOG, NETLIFYDEVERR } = require('./logo') const formatLambdaLocalError = (err) => `${err.errorType}: ${err.errorMessage}\n ${err.stackTrace.join('\n ')}` @@ -425,7 +425,7 @@ const getBuildFunction = ({ functionBuilder, log }) => } } -const setupFunctionsBuilder = async ({ config, errorExit, functionsDirectory, log, site, warn }) => { +const setupFunctionsBuilder = async ({ config, errorExit, functionsDirectory, log, site }) => { const functionBuilder = await detectFunctionsBuilder({ config, errorExit, @@ -443,9 +443,6 @@ const setupFunctionsBuilder = async ({ config, errorExit, functionsDirectory, lo : '' log(`${NETLIFYDEVLOG} Function builder ${chalk.yellow(functionBuilder.builderName)} detected${npmScriptString}.`) - warn( - `${NETLIFYDEVWARN} This is a beta feature, please give us feedback on how to improve at https://github.com/netlify/cli/`, - ) const debouncedBuild = debounce(getBuildFunction({ functionBuilder, log }), 300, { leading: true, @@ -487,7 +484,6 @@ const startFunctionsServer = async ({ config, settings, site, log, warn, errorEx functionsDirectory: settings.functions, log, site, - warn, }) const server = await getFunctionsServer({ dir: functionsDirectory || settings.functions,