Repository: netlify/zip-it-and-ship-it
Pull request #474 (2021-05-19 15:13:57)

feat: remove src directory from function bundle
**- Summary**

This PR removes the `src/` sub-directory from Node.js function deployments. This means that an input file at `{FUNCTIONS DIR}/file.js` will be placed at `{LAMBDA ROOT}/file.js` instead of `{LAMBDA ROOT}/src/file.js`.

The only exception to this is when there's a potential for a naming conflict, which might happen if there's a file at the root of the project with the same name as one of the functions _and_ that file is being implicitly (via `require`/`import`) or explicitly (via `functions.included_files`) included in the bundle. When this happens, `zip-it-and-ship-it` falls back to the current behaviour of placing all user-defined files inside a `src/` sub-directory.

Additionally, to ensure that paths are consistent between local and production environments, `zip-it-and-ship-it` now accepts an optional `basePath` property, which contains the shortest path that will be included in the bundle (in other words, the root path of the lambda, from which `process.cwd()` will be computed from).

_Example:_

```
/
  projects/
    my-site/ <---- PROJECT ROOT
      netlify/
        functions/
          func1.js
      netlify.toml
```

Currently, the base path will be `/projects/my-site/netlify/functions/func1`, so the function will live at `{LAMBDA ROOT}/src/func1.js` in production, whereas locally it will live at `{PROJECT ROOT}/netlify/functions/func1.js`.

With this PR, one can set the base path to be the project root directory (i.e. `/projects/my-site`), so the function file will live at `{LAMBDA ROOT}/netlify/functions/func1.js`, the same as locally.

Implements https://github.com/netlify/pod-workflow/issues/214.

**- Test plan**

Added a new test.

**- A picture of a cute animal (not mandatory but encouraged)**

![ba27b70e9e58fcbcf26a14578047cbb1](https://user-images.githubusercontent.com/4162329/119139947-7f821380-ba3b-11eb-81c5-286a81533220.jpg)


github-actions[bot]: ## ⏱ Benchmark results

Comparing with 9d5f1b83e9120cc6416f4f054836acc8ad4e5847

- **largeDepsEsbuild**: 11.5s ⬇️ (5.45% decrease)
- **largeDepsZisi**: 1m 4.3s ⬆️ (2.37% increase)
<!--delta:benchmark@{"largeDepsEsbuild":11579.558088333326,"largeDepsZisi":64313.847997666664}-->
eduardoboucas: I realised that while this is not a breaking change in the sense that we're not violating any of our public contracts, it does technically mean that any functions that are relying on the assumption that files will be placed under a `src/` directory might break.

For this reason, I've introduced an `experimentalHandlerV2` configuration property, which we can then hook up to `@netlify/config`, an environment variable or a feature flag (or a combination of these). Unless this property is set to `true`, the old behaviour is preserved, i.e. everything except the entry file will be placed under a `src/` directory.

This allows us to gradually roll this out and reduce any risks associated.

Patch:
diff --git a/src/runtimes/node/index.js b/src/runtimes/node/index.js
index 5c6f348..8aa0cc7 100644
--- a/src/runtimes/node/index.js
+++ b/src/runtimes/node/index.js
@@ -15,6 +15,7 @@ const getDefaultBundler = ({ extension }) => (extension === '.ts' ? JS_BUNDLER_E
 
 const zipFunction = async function ({
   archiveFormat,
+  basePath,
   config = {},
   destFolder,
   extension,
@@ -39,6 +40,7 @@ const zipFunction = async function ({
   if (bundler === JS_BUNDLER_ZISI) {
     return zipZisi({
       archiveFormat,
+      basePath,
       config,
       destFolder,
       extension,
@@ -53,6 +55,7 @@ const zipFunction = async function ({
 
   return zipEsbuild({
     archiveFormat,
+    basePath,
     config,
     destFolder,
     extension,
diff --git a/src/runtimes/node/src_files.js b/src/runtimes/node/src_files.js
index f066d47..2c43375 100644
--- a/src/runtimes/node/src_files.js
+++ b/src/runtimes/node/src_files.js
@@ -1,3 +1,4 @@
+const { normalize } = require('path')
 const { promisify } = require('util')
 
 const glob = require('glob')
@@ -35,8 +36,9 @@ const getPathsOfIncludedFiles = async (includedFiles, basePath) => {
   // `pathGroups` is an array containing the paths for each expression in the
   // `include` array. We flatten it into a single dimension.
   const paths = pathGroups.flat()
+  const normalizedPaths = paths.map(normalize)
 
-  return [...new Set(paths)]
+  return [...new Set(normalizedPaths)]
 }
 
 const getSrcFiles = async function ({ config, ...parameters }) {
diff --git a/src/runtimes/node/zip_esbuild.js b/src/runtimes/node/zip_esbuild.js
index 4da1c8a..66132ed 100644
--- a/src/runtimes/node/zip_esbuild.js
+++ b/src/runtimes/node/zip_esbuild.js
@@ -22,6 +22,19 @@ const getAliases = ({ bundlePath, mainFile, sourcemapPath, srcDir }) => {
   return aliases
 }
 
+const getFunctionBasePath = ({ basePathFromConfig, mainFile, supportingSrcFiles }) => {
+  // If there is a base path defined in the config, we use that.
+  if (basePathFromConfig !== undefined) {
+    return basePathFromConfig
+  }
+
+  // If not, the base path is the common path prefix between all the supporting
+  // files and the main file.
+  const dirnames = [...supportingSrcFiles, mainFile].map((filePath) => normalize(dirname(filePath)))
+
+  return commonPathPrefix(dirnames)
+}
+
 // Convenience method for retrieving external and ignored modules from
 // different places and merging them together.
 const getExternalAndIgnoredModules = async ({ config, srcDir }) => {
@@ -37,6 +50,7 @@ const getExternalAndIgnoredModules = async ({ config, srcDir }) => {
 
 const zipEsbuild = async ({
   archiveFormat,
+  basePath,
   config = {},
   destFolder,
   extension,
@@ -73,7 +87,7 @@ const zipEsbuild = async ({
     externalNodeModules: [...externalModules, ...Object.keys(nativeNodeModules)],
     bundler: JS_BUNDLER_ESBUILD,
     includedFiles: config.includedFiles,
-    includedFilesBasePath: config.includedFilesBasePath,
+    includedFilesBasePath: config.includedFilesBasePath || basePath,
     mainFile,
     srcPath,
     srcDir,
@@ -92,15 +106,15 @@ const zipEsbuild = async ({
   // We're adding the bundled file to the zip, but we want it to have the same
   // name and path as the original, unbundled file. For this, we use an alias.
   const aliases = getAliases({ bundlePath, mainFile: normalizedMainFile, sourcemapPath, srcDir })
-  const dirnames = supportingSrcFiles.map((filePath) => normalize(dirname(filePath)))
-  const basePath = commonPathPrefix([...dirnames, normalize(dirname(mainFile))])
+  const functionBasePath = getFunctionBasePath({ basePathFromConfig: basePath, mainFile, supportingSrcFiles })
 
   try {
     const path = await zipNodeJs({
       aliases,
       archiveFormat,
-      basePath,
+      basePath: functionBasePath,
       destFolder,
+      experimentalHandlerV2: config.experimentalHandlerV2,
       extension,
       filename,
       mainFile: normalizedMainFile,
diff --git a/src/runtimes/node/zip_zisi.js b/src/runtimes/node/zip_zisi.js
index 58b53f9..836df1e 100644
--- a/src/runtimes/node/zip_zisi.js
+++ b/src/runtimes/node/zip_zisi.js
@@ -9,6 +9,7 @@ const { getSrcFilesAndExternalModules } = require('./src_files')
 
 const zipZisi = async ({
   archiveFormat,
+  basePath,
   config,
   destFolder,
   extension,
@@ -23,7 +24,7 @@ const zipZisi = async ({
     bundler: JS_BUNDLER_ZISI,
     extension,
     includedFiles: config.includedFiles,
-    includedFilesBasePath: config.includedFilesBasePath,
+    includedFilesBasePath: config.includedFilesBasePath || basePath,
     mainFile,
     pluginsModulesPath,
     srcDir,
@@ -36,6 +37,7 @@ const zipZisi = async ({
     basePath: commonPathPrefix(dirnames),
     destFolder,
     extension,
+    experimentalHandlerV2: config.experimentalHandlerV2,
     filename,
     mainFile,
     pluginsModulesPath,
diff --git a/src/zip.js b/src/zip.js
index f62bdc5..f571d03 100644
--- a/src/zip.js
+++ b/src/zip.js
@@ -53,7 +53,7 @@ const formatZipResult = (result) => {
 const zipFunctions = async function (
   relativeSrcFolder,
   destFolder,
-  { archiveFormat = ARCHIVE_FORMAT_ZIP, config = {}, parallelLimit = DEFAULT_PARALLEL_LIMIT } = {},
+  { archiveFormat = ARCHIVE_FORMAT_ZIP, basePath, config = {}, parallelLimit = DEFAULT_PARALLEL_LIMIT } = {},
 ) {
   validateArchiveFormat(archiveFormat)
 
@@ -68,6 +68,7 @@ const zipFunctions = async function (
     async (func) => {
       const zipResult = await func.runtime.zipFunction({
         archiveFormat,
+        basePath,
         config: func.config,
         destFolder,
         extension: func.extension,
diff --git a/src/zip_node.js b/src/zip_node.js
index a02b66f..e5023f6 100644
--- a/src/zip_node.js
+++ b/src/zip_node.js
@@ -1,7 +1,7 @@
 const { Buffer } = require('buffer')
 const fs = require('fs')
 const os = require('os')
-const { basename, extname, join, normalize, sep } = require('path')
+const { basename, extname, join, normalize, resolve, sep } = require('path')
 const { promisify } = require('util')
 
 const copyFile = require('cp-file')
@@ -19,6 +19,10 @@ const pWriteFile = promisify(fs.writeFile)
 // Taken from https://www.npmjs.com/package/cpy.
 const COPY_FILE_CONCURRENCY = os.cpus().length === 0 ? 2 : os.cpus().length * 2
 
+// Sub-directory to place all user-defined files  (i.e. everything other than
+// the entry file generated by zip-it-and-ship-it).
+const DEFAULT_USER_SUBDIRECTORY = 'src'
+
 const createDirectory = async function ({
   aliases = new Map(),
   basePath,
@@ -33,6 +37,7 @@ const createDirectory = async function ({
     commonPrefix: basePath,
     filename,
     mainFile,
+    userNamespace: DEFAULT_USER_SUBDIRECTORY,
   })
   const functionFolder = join(destFolder, basename(filename, extension))
 
@@ -48,7 +53,12 @@ const createDirectory = async function ({
     srcFiles,
     (srcFile) => {
       const srcPath = aliases.get(srcFile) || srcFile
-      const normalizedSrcPath = normalizeFilePath(srcPath, basePath, pluginsModulesPath)
+      const normalizedSrcPath = normalizeFilePath({
+        commonPrefix: basePath,
+        path: srcPath,
+        pluginsModulesPath,
+        userNamespace: DEFAULT_USER_SUBDIRECTORY,
+      })
       const destPath = join(functionFolder, normalizedSrcPath)
 
       return copyFile(srcFile, destPath)
@@ -64,6 +74,7 @@ const createZipArchive = async function ({
   basePath,
   destFolder,
   extension,
+  experimentalHandlerV2,
   filename,
   mainFile,
   pluginsModulesPath,
@@ -71,15 +82,41 @@ const createZipArchive = async function ({
 }) {
   const destPath = join(destFolder, `${basename(filename, extension)}.zip`)
   const { archive, output } = startZip(destPath)
+  const entryFilename = `${basename(filename, extension)}.js`
+  const entryFilePath = resolve(basePath, entryFilename)
+
+  // Traditionally, everything other than the entry file was placed inside a
+  // `src` sub-directory. We want to change this behavior, but we're doing so
+  // gradually. For now, we keep injecting that sub-directory unless there is
+  // a `experimentalHandlerV2` configuration property set to `true`, in which
+  // case we place all files at the root.
+  const needsUserNamespace = experimentalHandlerV2 !== true
+
+  // We don't need an entry file if it would end up with the same path as the
+  // function's main file.
+  const needsEntryFile = needsUserNamespace || entryFilePath !== mainFile
+
+  // There is a naming conflict with the entry file if one of the supporting
+  // files (i.e. not the main file) has the path that the entry file needs to
+  // take.
+  const hasEntryFileConflict = srcFiles.some((srcFile) => srcFile === entryFilePath && srcFile !== mainFile)
+
+  // If there is a naming conflict, we move all user files (everything other
+  // than the entry file) to its own sub-directory.
+  const userNamespace = needsUserNamespace || hasEntryFileConflict ? DEFAULT_USER_SUBDIRECTORY : ''
 
-  addEntryFile(basePath, archive, filename, mainFile)
+  if (needsEntryFile) {
+    const entryFile = getEntryFile({ commonPrefix: basePath, filename, mainFile, userNamespace })
+
+    addEntryFileToZip(archive, entryFile)
+  }
 
   const srcFilesInfos = await Promise.all(srcFiles.map(addStat))
 
   // We ensure this is not async, so that the archive's checksum is
   // deterministic. Otherwise it depends on the order the files were added.
   srcFilesInfos.forEach(({ srcFile, stat }) => {
-    zipJsFile({ srcFile, commonPrefix: basePath, pluginsModulesPath, archive, stat, aliases })
+    zipJsFile({ srcFile, commonPrefix: basePath, pluginsModulesPath, archive, stat, aliases, userNamespace })
   })
 
   await endZip(archive, output)
@@ -95,11 +132,10 @@ const zipNodeJs = function ({ archiveFormat, ...options }) {
   return createDirectory(options)
 }
 
-const addEntryFile = function (commonPrefix, archive, filename, mainFile) {
-  const { contents: entryContents, filename: entryFilename } = getEntryFile({ commonPrefix, filename, mainFile })
-  const content = Buffer.from(entryContents)
+const addEntryFileToZip = function (archive, { contents, filename }) {
+  const contentBuffer = Buffer.from(contents)
 
-  addZipContent(archive, content, entryFilename)
+  addZipContent(archive, contentBuffer, filename)
 }
 
 const addStat = async function (srcFile) {
@@ -107,20 +143,28 @@ const addStat = async function (srcFile) {
   return { srcFile, stat }
 }
 
-const getEntryFile = ({ commonPrefix, filename, mainFile }) => {
-  const mainPath = normalizeFilePath(mainFile, commonPrefix)
+const getEntryFile = ({ commonPrefix, filename, mainFile, userNamespace }) => {
+  const mainPath = normalizeFilePath({ commonPrefix, path: mainFile, userNamespace })
   const extension = extname(filename)
   const entryFilename = `${basename(filename, extension)}.js`
 
   return {
-    contents: `module.exports = require('./${mainPath}')`,
+    contents: `module.exports = require('.${mainPath.startsWith('/') ? mainPath : `/${mainPath}`}')`,
     filename: entryFilename,
   }
 }
 
-const zipJsFile = function ({ srcFile, commonPrefix, pluginsModulesPath, archive, stat, aliases = new Map() }) {
+const zipJsFile = function ({
+  srcFile,
+  commonPrefix,
+  pluginsModulesPath,
+  archive,
+  stat,
+  aliases = new Map(),
+  userNamespace,
+}) {
   const filename = aliases.get(srcFile) || srcFile
-  const normalizedFilename = normalizeFilePath(filename, commonPrefix, pluginsModulesPath)
+  const normalizedFilename = normalizeFilePath({ commonPrefix, path: filename, pluginsModulesPath, userNamespace })
 
   addZipFile(archive, srcFile, normalizedFilename, stat)
 }
@@ -128,15 +172,16 @@ const zipJsFile = function ({ srcFile, commonPrefix, pluginsModulesPath, archive
 // `adm-zip` and `require()` expect Unix paths.
 // We remove the common path prefix.
 // With files on different Windows drives, we remove the drive letter.
-const normalizeFilePath = function (path, commonPrefix, pluginsModulesPath) {
+const normalizeFilePath = function ({ commonPrefix, path, pluginsModulesPath, userNamespace }) {
+  const userNamespacePathSegment = userNamespace ? `${userNamespace}${sep}` : ''
   const pathA = normalize(path)
   const pathB =
-    pluginsModulesPath === undefined ? pathA : pathA.replace(pluginsModulesPath, `${ZIP_ROOT_DIR}${sep}node_modules`)
-  const pathC = pathB.replace(commonPrefix, `${ZIP_ROOT_DIR}${sep}`)
+    pluginsModulesPath === undefined
+      ? pathA
+      : pathA.replace(pluginsModulesPath, `${userNamespacePathSegment}node_modules`)
+  const pathC = pathB.replace(commonPrefix, userNamespacePathSegment)
   const pathD = unixify(pathC)
   return pathD
 }
 
-const ZIP_ROOT_DIR = 'src'
-
 module.exports = { zipNodeJs }


Test patch:
diff --git a/tests/fixtures/base_path/content/post1.md b/tests/fixtures/base_path/content/post1.md
new file mode 100644
index 0000000..9bb4958
--- /dev/null
+++ b/tests/fixtures/base_path/content/post1.md
@@ -0,0 +1,1 @@
+# Hello from the other side
diff --git a/tests/fixtures/base_path/content/post2.md b/tests/fixtures/base_path/content/post2.md
new file mode 100644
index 0000000..b2bdaa5
--- /dev/null
+++ b/tests/fixtures/base_path/content/post2.md
@@ -0,0 +1,1 @@
+# I must've called a thousand times
diff --git a/tests/fixtures/base_path/content/post3.md b/tests/fixtures/base_path/content/post3.md
new file mode 100644
index 0000000..85adf76
--- /dev/null
+++ b/tests/fixtures/base_path/content/post3.md
@@ -0,0 +1,1 @@
+# To tell you I'm sorry for everything that I've done
diff --git a/tests/fixtures/base_path/func2.js b/tests/fixtures/base_path/func2.js
new file mode 100644
index 0000000..eb1b8e8
--- /dev/null
+++ b/tests/fixtures/base_path/func2.js
@@ -0,0 +1,1 @@
+module.exports = __dirname
diff --git a/tests/fixtures/base_path/netlify/functions1/func1.js b/tests/fixtures/base_path/netlify/functions1/func1.js
new file mode 100644
index 0000000..6610d2a
--- /dev/null
+++ b/tests/fixtures/base_path/netlify/functions1/func1.js
@@ -0,0 +1,1 @@
+module.exports = [__dirname]
diff --git a/tests/fixtures/base_path/netlify/functions2/func2.js b/tests/fixtures/base_path/netlify/functions2/func2.js
new file mode 100644
index 0000000..b851fdc
--- /dev/null
+++ b/tests/fixtures/base_path/netlify/functions2/func2.js
@@ -0,0 +1,3 @@
+const dep = require('../../func2')
+
+module.exports = [__dirname, dep]
diff --git a/tests/fixtures/base_path/node_modules/test/index.js b/tests/fixtures/base_path/node_modules/test/index.js
new file mode 100644
index 0000000..33c1891
--- /dev/null
+++ b/tests/fixtures/base_path/node_modules/test/index.js
@@ -0,0 +1,1 @@
+module.exports = true
diff --git a/tests/fixtures/base_path/node_modules/test/package.json b/tests/fixtures/base_path/node_modules/test/package.json
new file mode 100644
index 0000000..bd8dd31
--- /dev/null
+++ b/tests/fixtures/base_path/node_modules/test/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "test",
+  "version": "1.0.0"
+}
diff --git a/tests/fixtures/base_path/node_modules/test/test.html b/tests/fixtures/base_path/node_modules/test/test.html
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/base_path/node_modules/test/test.map b/tests/fixtures/base_path/node_modules/test/test.map
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/base_path/package.json b/tests/fixtures/base_path/package.json
new file mode 100644
index 0000000..02140e6
--- /dev/null
+++ b/tests/fixtures/base_path/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "fixture-with-included-files",
+  "version": "1.0.0",
+  "dependencies": {
+    "test": "1.0.0"
+  }
+}
diff --git a/tests/main.js b/tests/main.js
index 5591796..6d8cc7e 100644
--- a/tests/main.js
+++ b/tests/main.js
@@ -1020,6 +1020,103 @@ testBundlers(
   },
 )
 
+testBundlers(
+  'Places all user-defined files at the root of the target directory if the `experimentalHandlerV2` configuration property is defined',
+  [ESBUILD, ESBUILD_ZISI, DEFAULT],
+  async (bundler, t) => {
+    const fixtureName = 'base_path'
+    const { tmpDir } = await zipNode(t, `${fixtureName}/netlify/functions1`, {
+      opts: {
+        basePath: join(FIXTURES_DIR, fixtureName),
+        config: {
+          '*': {
+            experimentalHandlerV2: true,
+            includedFiles: ['content/*'],
+            nodeBundler: bundler,
+          },
+        },
+      },
+    })
+
+    // eslint-disable-next-line import/no-dynamic-require, node/global-require
+    const function1Entry = require(`${tmpDir}/func1.js`)
+
+    // The function should not be on a `src/` namespace.
+    t.false(unixify(function1Entry[0]).includes('/src/'))
+    t.false(await pathExists(`${tmpDir}/src/func1.js`))
+    t.true(await pathExists(`${tmpDir}/content/post1.md`))
+    t.true(await pathExists(`${tmpDir}/content/post2.md`))
+    t.true(await pathExists(`${tmpDir}/content/post3.md`))
+    t.false(await pathExists(`${tmpDir}/src/content/post1.md`))
+    t.false(await pathExists(`${tmpDir}/src/content/post2.md`))
+    t.false(await pathExists(`${tmpDir}/src/content/post3.md`))
+  },
+)
+
+testBundlers(
+  'Places all user-defined files in a `src/` sub-directory if there is a naming conflict with the entry file, even if the `experimentalHandlerV2` configuration property is defined',
+  [ESBUILD, ESBUILD_ZISI, DEFAULT],
+  async (bundler, t) => {
+    const fixtureName = 'base_path'
+    const { tmpDir } = await zipNode(t, `${fixtureName}/netlify/functions2`, {
+      opts: {
+        basePath: join(FIXTURES_DIR, fixtureName),
+        config: {
+          '*': {
+            experimentalHandlerV2: true,
+            includedFiles: ['content/*', 'func2.js'],
+            nodeBundler: bundler,
+          },
+        },
+      },
+    })
+
+    // eslint-disable-next-line import/no-dynamic-require, node/global-require
+    const function2Entry = require(`${tmpDir}/func2.js`)
+
+    // The function should be on a `src/` namespace because there's a conflict
+    // with the /func2.js path present in `includedFiles`.
+    t.true(unixify(function2Entry[0]).includes('/src/'))
+    t.true(await pathExists(`${tmpDir}/src/func2.js`))
+    t.false(await pathExists(`${tmpDir}/content/post1.md`))
+    t.false(await pathExists(`${tmpDir}/content/post2.md`))
+    t.false(await pathExists(`${tmpDir}/content/post3.md`))
+    t.true(await pathExists(`${tmpDir}/src/content/post1.md`))
+    t.true(await pathExists(`${tmpDir}/src/content/post2.md`))
+    t.true(await pathExists(`${tmpDir}/src/content/post3.md`))
+  },
+)
+
+testBundlers(
+  'Places all user-defined files in a `src/` sub-directory if the `experimentalHandlerV2` configuration property is not defined',
+  [ESBUILD, ESBUILD_ZISI, DEFAULT],
+  async (bundler, t) => {
+    const fixtureName = 'base_path'
+    const { tmpDir } = await zipNode(t, `${fixtureName}/netlify/functions2`, {
+      opts: {
+        basePath: join(FIXTURES_DIR, fixtureName),
+        config: {
+          '*': {
+            includedFiles: ['content/*'],
+            nodeBundler: bundler,
+          },
+        },
+      },
+    })
+
+    // eslint-disable-next-line import/no-dynamic-require, node/global-require
+    const function2Entry = require(`${tmpDir}/func2.js`)
+
+    t.true(unixify(function2Entry[0]).includes('/src/'))
+    t.false(await pathExists(`${tmpDir}/content/post1.md`))
+    t.false(await pathExists(`${tmpDir}/content/post2.md`))
+    t.false(await pathExists(`${tmpDir}/content/post3.md`))
+    t.true(await pathExists(`${tmpDir}/src/content/post1.md`))
+    t.true(await pathExists(`${tmpDir}/src/content/post2.md`))
+    t.true(await pathExists(`${tmpDir}/src/content/post3.md`))
+  },
+)
+
 test('When generating a directory for a function with `archiveFormat: "none"`, it empties the directory before copying any files', async (t) => {
   const { path: tmpDir } = await getTmpDir({ prefix: 'zip-it-test' })
   const functionDirectory = join(tmpDir, 'function')