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..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, diff --git a/src/function-builder-detectors/zisi.js b/src/function-builder-detectors/zisi.js new file mode 100644 index 00000000000..c3112dec247 --- /dev/null +++ b/src/function-builder-detectors/zisi.js @@ -0,0 +1,71 @@ +const path = require('path') + +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 ({ errorExit }) => { + const targetDirectory = path.resolve(getPathInProject(['functions-serve'])) + + try { + await makeDir(targetDirectory) + } catch (error) { + errorExit(`${NETLIFYDEVERR} Could not create directory: ${targetDirectory}`) + } + + return targetDirectory +} + +module.exports = async function handler({ config, errorExit, functionsDirectory: sourceDirectory }) { + 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 }), + builderName: 'zip-it-and-ship-it', + omitFileChangesLog: true, + src: sourceDirectory, + target: targetDirectory, + } +} diff --git a/src/utils/detect-functions-builder.js b/src/utils/detect-functions-builder.js index 084fb2ddb77..c3bf5054db3 100644 --- a/src/utils/detect-functions-builder.js +++ b/src/utils/detect-functions-builder.js @@ -1,17 +1,19 @@ 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 .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}`))) 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 ac99f40a6db..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 ')}` @@ -153,12 +153,18 @@ const buildClientContext = function (headers) { } } -const clearCache = (action) => (path) => { - console.log(`${NETLIFYDEVLOG} ${path} ${action}, reloading...`) +const clearCache = ({ action, omitLog }) => (path) => { + if (!omitLog) { + console.log(`${NETLIFYDEVLOG} ${path} ${action}, reloading...`) + } + Object.keys(require.cache).forEach((key) => { delete require.cache[key] }) - console.log(`${NETLIFYDEVLOG} ${path} ${action}, successfully reloaded!`) + + if (!omitLog) { + console.log(`${NETLIFYDEVLOG} ${path} ${action}, successfully reloaded!`) + } } const shouldBase64Encode = function (contentType) { @@ -173,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('modified')).on('unlink', clearCache('deleted')) + watcher + .on('change', clearCache({ action: 'modified', omitLog: omitFileChangesLog })) + .on('unlink', clearCache({ action: 'deleted', omitLog: omitFileChangesLog })) const logger = winston.createLogger({ levels: winston.config.npm.levels, @@ -363,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') @@ -385,13 +393,13 @@ 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 } const getBuildFunction = ({ functionBuilder, log }) => - async function build() { + async function build(updatedPath) { log( `${NETLIFYDEVLOG} Function builder ${chalk.yellow(functionBuilder.builderName)} ${chalk.magenta( 'building', @@ -399,7 +407,7 @@ const getBuildFunction = ({ functionBuilder, log }) => ) try { - await functionBuilder.build() + await functionBuilder.build(updatedPath) log( `${NETLIFYDEVLOG} Function builder ${chalk.yellow(functionBuilder.builderName)} ${chalk.green( 'finished', @@ -417,32 +425,40 @@ 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 setupFunctionsBuilder = async ({ config, errorExit, functionsDirectory, log, site }) => { + const functionBuilder = await detectFunctionsBuilder({ + config, + errorExit, + functionsDirectory, + log, + projectRoot: site.root, + }) - const debouncedBuild = debounce(getBuildFunction({ functionBuilder, log }), 300, { - leading: true, - trailing: true, - }) + if (!functionBuilder) { + return {} + } - await debouncedBuild() + const npmScriptString = functionBuilder.npmScript + ? `: Running npm script ${chalk.yellow(functionBuilder.npmScript)}` + : '' - const functionWatcher = chokidar.watch(functionBuilder.src) - functionWatcher.on('ready', () => { - functionWatcher.on('add', debouncedBuild) - functionWatcher.on('change', debouncedBuild) - functionWatcher.on('unlink', debouncedBuild) - }) - } + log(`${NETLIFYDEVLOG} Function builder ${chalk.yellow(functionBuilder.builderName)} detected${npmScriptString}.`) + + 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 +474,25 @@ const startServer = async ({ server, settings, log, errorExit }) => { }) } -const startFunctionsServer = async ({ settings, site, log, warn, errorExit, siteUrl, capabilities }) => { +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 server = await getFunctionsServer({ dir: settings.functions, siteUrl, capabilities, warn }) + const { omitFileChangesLog, target: functionsDirectory } = await setupFunctionsBuilder({ + config, + errorExit, + functionsDirectory: settings.functions, + log, + site, + }) + const server = await getFunctionsServer({ + dir: functionsDirectory || settings.functions, + omitFileChangesLog, + siteUrl, + capabilities, + warn, + }) + await startServer({ server, settings, log, errorExit }) } } 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 */