Repository: netlify/zip-it-and-ship-it
Pull request #537 (2021-06-17 08:51:01)

feat: add support for multiple source directories
**- Summary**

Makes `zipFunctions` capable of bundling functions from multiple directories if the first argument is an array. When this happens, the order of the directories in the arrays matters, as rightmost directories will take precedence in case of naming conflicts.

**- Test plan**

Added a new test.

**- A picture of a cute animal (not mandatory but encouraged)**

![armadillo](https://user-images.githubusercontent.com/4162329/122364173-72304a00-cf51-11eb-9f37-ee14331b5223.jpg)

(cc @ascorbic)


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

*Comparing with 508721c3c2e3af2d672da86172686aa2e4e0776c*

### largeDepsEsbuild: 11.5s
 ⬇️ **7.78% decrease** vs. 508721c3c2e3af2d672da86172686aa2e4e0776c
```
^                                                                                          12.5s          
│                   12s     12s                                    11.6s           12.1s    ┌──┐          
│                   ┌──┐    ┌──┐           11.4s   11.3s   11.5s    ┌──┐            ┌──┐    |  |   11.5s  
│                   |  |    |  |            ┌──┐    ┌──┐    ┌──┐    |  |            |  |    |  |    ┌──┐  
│   9.9s    10s     |  |    |  |    9.7s    |  |    |  |    |  |    |  |            |  |    |  |    |▒▒|  
│   ┌──┐    ┌──┐    |  |    |  |    ┌──┐    |  |    |  |    |  |    |  |    9.6s    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    ┌──┐    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
└───┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴──>
    T-12    T-11    T-10    T-9     T-8     T-7     T-6     T-5     T-4     T-3     T-2     T-1      T    
```
<details>
<summary>Legend</summary>

- T-30 (508721c3c2e3af2d672da86172686aa2e4e0776c): 12.5s
- T-29 (790010f035f121286c189ac88cf49e5a51de7a1b): 10.6s
- T-28 (0dfcc70a334d09523dcb127e1812d0dbbda7481b): 12s
- T-27 (c4c746323af0ffa3706c6136675aebbac2af1e3d): 10.3s
- T-26 (f1363d24abeeeb11abaed0c564ca5ae8874a70ae): 11.7s
- T-25 (a5cef00715188d477ba69f033c16de00cea768c0): 13s
- T-24 (febeffcae56016e74348c4380fd5572edcbe84d0): 12.5s
- T-23 (991d9c3ab2e4e4da188024e3e005601ce9f14896): 12.3s
- T-22 (dd7dd81e5bf63586018d0ef384c399b2c1ba21c4): 11.6s
- T-21 (d4bcaf0ab1ab6f984d4658da105936a21011fe66): 12s
- T-20 (92f64a67160ac9eb0301ae738e38d258117f2b06): 10.3s
- T-19 (06bc4f69b05c96899a1efbd953c560219291b568): 11.6s
- T-18 (6c0e5f1059d5e672ea5ee6509150e0896030e745): 12.1s
- T-17 (e4c22192f1f66aae395b00fe8eaf4c886781d723): 11.4s
- T-16 (3ba92859e8fbf0f20abc46912284575c24e18124): 12s
- T-15 (5cdd4d23e88300d026f0152222136a35849c3b7f): 11.5s
- T-14 (df7f3e92e9547649a4c7c0b25d6e317af292c089): 12.7s
- T-13 (08f2a5d49a78e4424bc7478aa3cc8c98d21c5ee7): 9.9s
- T-12 (1e5ba0b8519f8ab643496979d7304d68d749d31e): 9.9s
- T-11 (1507586389e536db74867e3e8d3a7145373adfd3): 10s
- T-10 (6f4ab2998291fc3dd9dfa2d48d38be4d5ab8a901): 12s
- T-9 (d28f357f9dc98d7a0b278b99fee81daaa1e2c434): 12s
- T-8 (0687ba92a59971f75e554b70b407a85540f35616): 9.7s
- T-7 (98d18d9aa251f7f5758f55b3da27ccf066f98cc0): 11.4s
- T-6 (3de38a4f1d3c5c4b64c87a47926926a7812319e4): 11.3s
- T-5 (ab9f1d536555739628655ad674d4c0452ca47218): 11.5s
- T-4 (b248dfa11282ff23bd19001aa1ff66bd38a93d0b): 11.6s
- T-3 (c296d1706906a5d7b820cd7fb625c06453503f0e): 9.6s
- T-2 (62d7be931cf51cfb5cf5dba60a463678d69af4ce): 12.1s
- T-1 (8392f530c234ed8fa2881bad5c1ad7c937d7b588): 12.5s
- **T (current commit): 11.5s**</details>
### largeDepsZisi: 1m 0.2s
 ⬇️ **4.73% decrease** vs. 508721c3c2e3af2d672da86172686aa2e4e0776c
```
^                                                                                         1m 9.7s         
│                 1m 5.4s 1m 5.9s                                                 1m 5.5s   ┌──┐          
│                   ┌──┐    ┌──┐                                  1m 1.8s           ┌──┐    |  |          
│                   |  |    |  |          1m 0.6s 1m 0.1s 1m 0.3s   ┌──┐            |  |    |  |  1m 0.2s 
│          54.8s    |  |    |  |            ┌──┐    ┌──┐    ┌──┐    |  |            |  |    |  |    ┌──┐  
│  52.8s    ┌──┐    |  |    |  |   52.2s    |  |    |  |    |  |    |  |   51.8s    |  |    |  |    |▒▒|  
│   ┌──┐    |  |    |  |    |  |    ┌──┐    |  |    |  |    |  |    |  |    ┌──┐    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
└───┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴──>
    T-12    T-11    T-10    T-9     T-8     T-7     T-6     T-5     T-4     T-3     T-2     T-1      T    
```
<details>
<summary>Legend</summary>

- T-30 (508721c3c2e3af2d672da86172686aa2e4e0776c): 1m 3.1s
- T-29 (790010f035f121286c189ac88cf49e5a51de7a1b): 54s
- T-28 (0dfcc70a334d09523dcb127e1812d0dbbda7481b): 1m 3.9s
- T-27 (c4c746323af0ffa3706c6136675aebbac2af1e3d): 54.3s
- T-26 (f1363d24abeeeb11abaed0c564ca5ae8874a70ae): 59.5s
- T-25 (a5cef00715188d477ba69f033c16de00cea768c0): 1m 9.5s
- T-24 (febeffcae56016e74348c4380fd5572edcbe84d0): 1m 3.7s
- T-23 (991d9c3ab2e4e4da188024e3e005601ce9f14896): 1m 2.9s
- T-22 (dd7dd81e5bf63586018d0ef384c399b2c1ba21c4): 59.5s
- T-21 (d4bcaf0ab1ab6f984d4658da105936a21011fe66): 1m 7.1s
- T-20 (92f64a67160ac9eb0301ae738e38d258117f2b06): 54.7s
- T-19 (06bc4f69b05c96899a1efbd953c560219291b568): 1m 4.4s
- T-18 (6c0e5f1059d5e672ea5ee6509150e0896030e745): 1m 3.6s
- T-17 (e4c22192f1f66aae395b00fe8eaf4c886781d723): 59.3s
- T-16 (3ba92859e8fbf0f20abc46912284575c24e18124): 1m 2.7s
- T-15 (5cdd4d23e88300d026f0152222136a35849c3b7f): 1m 2.4s
- T-14 (df7f3e92e9547649a4c7c0b25d6e317af292c089): 1m 6.3s
- T-13 (08f2a5d49a78e4424bc7478aa3cc8c98d21c5ee7): 52.2s
- T-12 (1e5ba0b8519f8ab643496979d7304d68d749d31e): 52.8s
- T-11 (1507586389e536db74867e3e8d3a7145373adfd3): 54.8s
- T-10 (6f4ab2998291fc3dd9dfa2d48d38be4d5ab8a901): 1m 5.4s
- T-9 (d28f357f9dc98d7a0b278b99fee81daaa1e2c434): 1m 5.9s
- T-8 (0687ba92a59971f75e554b70b407a85540f35616): 52.2s
- T-7 (98d18d9aa251f7f5758f55b3da27ccf066f98cc0): 1m 0.6s
- T-6 (3de38a4f1d3c5c4b64c87a47926926a7812319e4): 1m 0.1s
- T-5 (ab9f1d536555739628655ad674d4c0452ca47218): 1m 0.3s
- T-4 (b248dfa11282ff23bd19001aa1ff66bd38a93d0b): 1m 1.8s
- T-3 (c296d1706906a5d7b820cd7fb625c06453503f0e): 51.8s
- T-2 (62d7be931cf51cfb5cf5dba60a463678d69af4ce): 1m 5.5s
- T-1 (8392f530c234ed8fa2881bad5c1ad7c937d7b588): 1m 9.7s
- **T (current commit): 1m 0.2s**</details>
<!--delta:benchmark@{}-->
ascorbic: Nice! Have we decided how we'll expose this in builds? Just add an automatic `.netlify/functions` dir, or allow plugins/toml to add multiple funcitons folders?
eduardoboucas: > Nice! Have we decided how we'll expose this in builds? Just add an automatic `.netlify/functions` dir, or allow plugins/toml to add multiple funcitons folders?

I think the former would be simpler, but I'm keeping the implementation in zip-it-and-ship-it generic enough that it can support both scenarios.

Patch:
diff --git a/README.md b/README.md
index 2a9c51e..930d60c 100644
--- a/README.md
+++ b/README.md
@@ -24,9 +24,9 @@ npm install @netlify/zip-it-and-ship-it
 
 # Usage (Node.js)
 
-## zipFunctions(srcFolder, destFolder, options?)
+## zipFunctions(srcFolders, destFolder, options?)
 
-- `srcFolder`: `string`
+- `srcFolders`: `string` | `Array<string>`
 - `destFolder`: `string`
 - `options`: `object?`
 - _Return value_: `Promise<object[]>`
@@ -45,12 +45,15 @@ const zipNetlifyFunctions = async function () {
 
 Creates Zip `archives` from Node.js, Go, and Rust programs. Those `archives` are ready to be uploaded to AWS Lambda.
 
-### `srcFolder`
+### `srcFolders`
+
+A directory or a list of directories containing the source files. If a string is provided, the corresponding directory
+must exist. If an array of strings is provided, at least one directory must exist.
 
-The directory containing the source files. It must exist. In Netlify, this is the
+In Netlify, this directory is the
 ["Functions folder"](https://docs.netlify.com/functions/configure-and-deploy/#configure-the-functions-folder).
 
-`srcFolder` can contain:
+A source folder can contain:
 
 - Sub-directories with a main file called `index.js`, `index.ts`, `{dir}.js` or `{dir}.ts` where `{dir}` is the
   sub-directory name.
diff --git a/src/utils/fs.js b/src/utils/fs.js
index 5ccff0e..fe9ae75 100644
--- a/src/utils/fs.js
+++ b/src/utils/fs.js
@@ -1,5 +1,5 @@
 const { readdir, unlink } = require('fs')
-const { format, join, parse } = require('path')
+const { format, join, parse, resolve } = require('path')
 const { promisify } = require('util')
 
 const pReaddir = promisify(readdir)
@@ -13,6 +13,30 @@ const safeUnlink = async (path) => {
   } catch (_) {}
 }
 
+// Takes a list of absolute paths and returns an array containing all the
+// filenames within those directories, if at least one of the directories
+// exists. If not, an error is thrown.
+const listFunctionsDirectories = async function (srcFolders) {
+  const filenamesByDirectory = await Promise.all(
+    srcFolders.map(async (srcFolder) => {
+      try {
+        const filenames = await listFunctionsDirectory(srcFolder)
+
+        return filenames
+      } catch (error) {
+        return null
+      }
+    }),
+  )
+  const validDirectories = filenamesByDirectory.filter(Boolean)
+
+  if (validDirectories.length === 0) {
+    throw new Error(`Functions folder does not exist: ${srcFolders.join(', ')}`)
+  }
+
+  return validDirectories.flat()
+}
+
 const listFunctionsDirectory = async function (srcFolder) {
   try {
     const filenames = await pReaddir(srcFolder)
@@ -23,4 +47,17 @@ const listFunctionsDirectory = async function (srcFolder) {
   }
 }
 
-module.exports = { getPathWithExtension, listFunctionsDirectory, safeUnlink }
+const resolveFunctionsDirectories = (input) => {
+  const directories = Array.isArray(input) ? input : [input]
+  const absoluteDirectories = directories.map((srcFolder) => resolve(srcFolder))
+
+  return absoluteDirectories
+}
+
+module.exports = {
+  getPathWithExtension,
+  listFunctionsDirectories,
+  listFunctionsDirectory,
+  resolveFunctionsDirectories,
+  safeUnlink,
+}
diff --git a/src/zip.js b/src/zip.js
index 559264d..c6af48f 100644
--- a/src/zip.js
+++ b/src/zip.js
@@ -7,7 +7,7 @@ const { getFlags } = require('./feature_flags')
 const { getPluginsModulesPath } = require('./node_dependencies')
 const { getFunctionsFromPaths } = require('./runtimes')
 const { ARCHIVE_FORMAT_NONE, ARCHIVE_FORMAT_ZIP } = require('./utils/consts')
-const { listFunctionsDirectory } = require('./utils/fs')
+const { listFunctionsDirectories, resolveFunctionsDirectories } = require('./utils/fs')
 const { removeFalsy } = require('./utils/remove_falsy')
 
 const DEFAULT_PARALLEL_LIMIT = 5
@@ -52,7 +52,7 @@ const formatZipResult = (result) => {
 // Zip `srcFolder/*` (Node.js or Go files) to `destFolder/*.zip` so it can be
 // used by AWS Lambda
 const zipFunctions = async function (
-  relativeSrcFolder,
+  relativeSrcFolders,
   destFolder,
   {
     archiveFormat = ARCHIVE_FORMAT_ZIP,
@@ -65,11 +65,15 @@ const zipFunctions = async function (
   validateArchiveFormat(archiveFormat)
 
   const featureFlags = getFlags(inputFeatureFlags)
-  const srcFolder = resolve(relativeSrcFolder)
-  const [paths] = await Promise.all([listFunctionsDirectory(srcFolder), makeDir(destFolder)])
+  const srcFolders = resolveFunctionsDirectories(relativeSrcFolders)
+  const [paths] = await Promise.all([listFunctionsDirectories(srcFolders), makeDir(destFolder)])
   const [functions, pluginsModulesPath] = await Promise.all([
     getFunctionsFromPaths(paths, { config, dedupe: true, featureFlags }),
-    getPluginsModulesPath(srcFolder),
+
+    // All function directories should be part of the same project root, so we
+    // can get away with computing the plugins modules path from any of the
+    // source directories.
+    getPluginsModulesPath(srcFolders[0]),
   ])
   const zipped = await pMap(
     functions.values(),


Test patch:
diff --git a/tests/fixtures/multiple-src-directories/.netlify/internal-functions/function.js b/tests/fixtures/multiple-src-directories/.netlify/internal-functions/function.js
new file mode 100644
index 0000000..aa71072
--- /dev/null
+++ b/tests/fixtures/multiple-src-directories/.netlify/internal-functions/function.js
@@ -0,0 +1,3 @@
+const mod = require('test')
+
+module.exports = mod && 'internal'
diff --git a/tests/fixtures/multiple-src-directories/.netlify/internal-functions/function_internal.js b/tests/fixtures/multiple-src-directories/.netlify/internal-functions/function_internal.js
new file mode 100644
index 0000000..aa71072
--- /dev/null
+++ b/tests/fixtures/multiple-src-directories/.netlify/internal-functions/function_internal.js
@@ -0,0 +1,3 @@
+const mod = require('test')
+
+module.exports = mod && 'internal'
diff --git a/tests/fixtures/multiple-src-directories/netlify/functions/function.js b/tests/fixtures/multiple-src-directories/netlify/functions/function.js
new file mode 100644
index 0000000..7558ca1
--- /dev/null
+++ b/tests/fixtures/multiple-src-directories/netlify/functions/function.js
@@ -0,0 +1,3 @@
+const mod = require('test')
+
+module.exports = mod && 'user'
diff --git a/tests/fixtures/multiple-src-directories/netlify/functions/function_user.js b/tests/fixtures/multiple-src-directories/netlify/functions/function_user.js
new file mode 100644
index 0000000..7558ca1
--- /dev/null
+++ b/tests/fixtures/multiple-src-directories/netlify/functions/function_user.js
@@ -0,0 +1,3 @@
+const mod = require('test')
+
+module.exports = mod && 'user'
diff --git a/tests/fixtures/multiple-src-directories/node_modules/test/index.js b/tests/fixtures/multiple-src-directories/node_modules/test/index.js
new file mode 100644
index 0000000..33c1891
--- /dev/null
+++ b/tests/fixtures/multiple-src-directories/node_modules/test/index.js
@@ -0,0 +1,1 @@
+module.exports = true
diff --git a/tests/fixtures/multiple-src-directories/node_modules/test/package.json b/tests/fixtures/multiple-src-directories/node_modules/test/package.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/tests/fixtures/multiple-src-directories/node_modules/test/package.json
@@ -0,0 +1,1 @@
+{}
diff --git a/tests/fixtures/multiple-src-directories/package.json b/tests/fixtures/multiple-src-directories/package.json
new file mode 100644
index 0000000..5416bb9
--- /dev/null
+++ b/tests/fixtures/multiple-src-directories/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "local-node-module",
+  "version": "1.0.0",
+  "dependencies": {
+    "test": "1.0.2"
+  }
+}
diff --git a/tests/helpers/main.js b/tests/helpers/main.js
index bc5724c..342bcf0 100644
--- a/tests/helpers/main.js
+++ b/tests/helpers/main.js
@@ -42,7 +42,10 @@ const zipFixture = async function (t, fixture, { length, fixtureDir, opts = {} }
 }
 
 const zipCheckFunctions = async function (t, fixture, { length = 1, fixtureDir = FIXTURES_DIR, tmpDir, opts } = {}) {
-  const files = await zipFunctions(`${fixtureDir}/${fixture}`, tmpDir, opts)
+  const srcFolders = Array.isArray(fixture)
+    ? fixture.map((srcFolder) => `${fixtureDir}/${srcFolder}`)
+    : `${fixtureDir}/${fixture}`
+  const files = await zipFunctions(srcFolders, tmpDir, opts)
 
   t.true(Array.isArray(files))
   t.is(files.length, length)
diff --git a/tests/main.js b/tests/main.js
index 02c5465..cd6a5e1 100644
--- a/tests/main.js
+++ b/tests/main.js
@@ -1,6 +1,6 @@
 const { readFile, chmod, symlink, unlink, rename, stat, writeFile } = require('fs')
 const { tmpdir } = require('os')
-const { join, normalize, resolve } = require('path')
+const { dirname, join, normalize, resolve } = require('path')
 const { env, platform, versions } = require('process')
 const { promisify } = require('util')
 
@@ -1143,6 +1143,50 @@ testBundlers(
   },
 )
 
+testBundlers(
+  'Bundles functions from multiple directories when the first argument of `zipFunctions()` is an array',
+  [ESBUILD, ESBUILD_ZISI, DEFAULT],
+  async (bundler, t) => {
+    const fixtureName = 'multiple-src-directories'
+    const pathInternal = `${fixtureName}/.netlify/internal-functions`
+    const pathUser = `${fixtureName}/netlify/functions`
+    const { files, tmpDir } = await zipNode(t, [pathInternal, pathUser], {
+      length: 3,
+      opts: {
+        basePath: join(FIXTURES_DIR, fixtureName),
+        config: {
+          '*': {
+            nodeBundler: bundler,
+          },
+        },
+      },
+    })
+
+    /* eslint-disable import/no-dynamic-require, node/global-require */
+    const functionCommon = require(`${tmpDir}/function.js`)
+    const functionInternal = require(`${tmpDir}/function_internal.js`)
+    const functionUser = require(`${tmpDir}/function_user.js`)
+    /* eslint-enable import/no-dynamic-require, node/global-require */
+
+    // Functions from rightmost directories in the array take precedence.
+    t.is(functionCommon, 'user')
+    t.is(functionInternal, 'internal')
+    t.is(functionUser, 'user')
+
+    const functionCommonEntry = files.find(({ name }) => name === 'function')
+    const functionInternalEntry = files.find(({ name }) => name === 'function_internal')
+    const functionUserEntry = files.find(({ name }) => name === 'function_user')
+
+    t.not(functionCommonEntry, undefined)
+    t.not(functionInternalEntry, undefined)
+    t.not(functionUserEntry, undefined)
+
+    t.is(dirname(functionCommonEntry.mainFile), resolve(join(__dirname, 'fixtures', pathUser)))
+    t.is(dirname(functionInternalEntry.mainFile), resolve(join(__dirname, 'fixtures', pathInternal)))
+    t.is(dirname(functionUserEntry.mainFile), resolve(join(__dirname, 'fixtures', pathUser)))
+  },
+)
+
 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')