Repository: netlify/zip-it-and-ship-it
Pull request #659 (2021-09-17 14:48:31)

feat: replace precinct with esbuild
**- Summary**

Replaces `precinct` with esbuild, purely for its parsing capabilities, in the legacy bundler flow.

Closes https://github.com/netlify/pod-serverless/issues/39.

**- Test plan**

N/A

**- A picture of a cute animal (not mandatory but encouraged)**

![gophers-1924099_1920-1-1920x960](https://user-images.githubusercontent.com/4162329/133802982-8c5cadc6-fd30-4ddc-9add-06c2fde06c04.jpg)


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

*Comparing with b105692e48b8ff2c175a7f93e9e28b053c8b5453*

### largeDepsEsbuild: 12s
 ⬇️ **3.74% decrease** vs. b105692e48b8ff2c175a7f93e9e28b053c8b5453
```
^  14.2s                                                                                                  
│   ┌──┐   13.5s   13.2s           13.3s   13.2s                           13.3s                          
│   |  |    ┌──┐    ┌──┐            ┌──┐    ┌──┐                            ┌──┐                          
│   |  |    |  |    |  |            |  |    |  |                            |  |                    12s   
│ ──┼──┼────┼──┼────┼──┼───11.1s────┼──┼────┼──┼───────────11.7s───11.1s────┼──┼───11.6s────────────┌──┐──
│   |  |    |  |    |  |    ┌──┐    |  |    |  |   10.8s    ┌──┐    ┌──┐    |  |    ┌──┐            |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    ┌──┐    |  |    |  |    |  |    |  |   10.2s    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    ┌──┐    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
└───┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴──>
    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 (b105692e48b8ff2c175a7f93e9e28b053c8b5453): 12.4s
- T-29 (9b0c6d56e4e05704122fdd55cacc81d75cf40aa1): 10.6s
- T-28 (53d0281332b686165f2f3660b2c51efff5fea2a2): 12.2s
- T-27 (b468b2abe161bdd1e330b0f73bede56b497565cf): 11.1s
- T-26 (a9b86e2f9de9b0929dda849afee625651b4dc7cb): 11.1s
- T-25 (4c64157467b1e5186bb66a7af8cca71c2b8ad633): 11.5s
- T-24 (fbcb80f1a13b5c633331a51f5a8fdb00cebad0d8): 11.3s
- T-23 (cde6c632dff6ba227cf1b3017a1c6e0dced82c11): 12.4s
- T-22 (c1e762d844569d87cf049a04999e9efeb1ece034): 13.6s
- T-21 (cc489697c9efa5994b187ba4a3271d2d3ef89ad4): 13s
- T-20 (ce3c162cbbca918748763b66df2e29065bb41e47): 13.7s
- T-19 (fc6b3efdc887db8ed20bffa594cc953085ade70e): 10.3s
- T-18 (cde4e512323f9859ab4db145f30b091984cdaad4): 13.9s
- T-17 (cde22957f16be7748d3090f72da98d74a0725f1e): 13.9s
- T-16 (e463691e3e5b289ee7adc941fb8f7c6974effa31): 11.4s
- T-15 (ecb8856c139b7aa45aa866050cb0559e1e30ee37): 10.1s
- T-14 (73456ffd767b829916e66df531db54dd21398db2): 12.8s
- T-13 (452f7ee6a7da26f337079d531a1a0a061ea0abeb): 14.1s
- T-12 (fcc82b753d9562aeaa252a8b99aa962b30ab7b9f): 14.2s
- T-11 (0862f9a17bbcc13ec13e2d1b52dc8548a72b2f99): 13.5s
- T-10 (8227298f59efec76b30e2969ef55b6d7d66935cc): 13.2s
- T-9 (4bec0a78760c82c52dd8b352abfd286306dacd80): 11.1s
- T-8 (ccea887d6ce4d32a05319ea04bbdeb8fd7ac8073): 13.3s
- T-7 (d9c033420961859bd13239888f23396e6b02da45): 13.2s
- T-6 (58a459ea75a804c4c39002135cb5cbc9ff4bb677): 10.8s
- T-5 (0b084549ee3eb5c4dae9477d33ff10be8a533084): 11.7s
- T-4 (56f4d4a39cbdee23a5856037430ab627ce37ab22): 11.1s
- T-3 (7b77c5668a7ad785310d7c46010c2863935aaa9a): 13.3s
- T-2 (4a5a4922e0b8793c8b01af6aa55915ba518bc9bf): 11.6s
- T-1 (38f4d4753a7205b3ec242440b3d2a1bbd866131a): 10.2s
- **T (current commit): 12s**</details>
### largeDepsZisi: 24.6s
 ⬇️ **172.62% decrease** vs. b105692e48b8ff2c175a7f93e9e28b053c8b5453
```
^ 1m 14.8s                                                                 1m 14s                         
│   ┌──┐  1m 11.3s1m 9.5s         1m 12.5s1m 11.3s                          ┌──┐                          
│   |  |    ┌──┐    ┌──┐            ┌──┐    ┌──┐                            |  |  1m 6.9s                 
│   |  |    |  |    |  |  1m 2.7s   |  |    |  |           1m 3s            |  |    ┌──┐                  
│ ──┼──┼────┼──┼────┼──┼────┌──┐────┼──┼────┼──┼────59s─────┌──┐───58.2s────┼──┼────┼──┼───59.9s──────────
│   |  |    |  |    |  |    |  |    |  |    |  |    ┌──┐    |  |    ┌──┐    |  |    |  |    ┌──┐          
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |          
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |          
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |          
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |          
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |          
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |          
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |          
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |   24.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 (b105692e48b8ff2c175a7f93e9e28b053c8b5453): 1m 7.2s
- T-29 (9b0c6d56e4e05704122fdd55cacc81d75cf40aa1): 58s
- T-28 (53d0281332b686165f2f3660b2c51efff5fea2a2): 1m 5.7s
- T-27 (b468b2abe161bdd1e330b0f73bede56b497565cf): 59.1s
- T-26 (a9b86e2f9de9b0929dda849afee625651b4dc7cb): 57.6s
- T-25 (4c64157467b1e5186bb66a7af8cca71c2b8ad633): 58s
- T-24 (fbcb80f1a13b5c633331a51f5a8fdb00cebad0d8): 56.5s
- T-23 (cde6c632dff6ba227cf1b3017a1c6e0dced82c11): 1m 7.4s
- T-22 (c1e762d844569d87cf049a04999e9efeb1ece034): 1m 10.3s
- T-21 (cc489697c9efa5994b187ba4a3271d2d3ef89ad4): 1m 8s
- T-20 (ce3c162cbbca918748763b66df2e29065bb41e47): 1m 11.6s
- T-19 (fc6b3efdc887db8ed20bffa594cc953085ade70e): 52.9s
- T-18 (cde4e512323f9859ab4db145f30b091984cdaad4): 1m 13.5s
- T-17 (cde22957f16be7748d3090f72da98d74a0725f1e): 1m 14.3s
- T-16 (e463691e3e5b289ee7adc941fb8f7c6974effa31): 57.6s
- T-15 (ecb8856c139b7aa45aa866050cb0559e1e30ee37): 50.8s
- T-14 (73456ffd767b829916e66df531db54dd21398db2): 1m 7.8s
- T-13 (452f7ee6a7da26f337079d531a1a0a061ea0abeb): 1m 10.9s
- T-12 (fcc82b753d9562aeaa252a8b99aa962b30ab7b9f): 1m 14.8s
- T-11 (0862f9a17bbcc13ec13e2d1b52dc8548a72b2f99): 1m 11.3s
- T-10 (8227298f59efec76b30e2969ef55b6d7d66935cc): 1m 9.5s
- T-9 (4bec0a78760c82c52dd8b352abfd286306dacd80): 1m 2.7s
- T-8 (ccea887d6ce4d32a05319ea04bbdeb8fd7ac8073): 1m 12.5s
- T-7 (d9c033420961859bd13239888f23396e6b02da45): 1m 11.3s
- T-6 (58a459ea75a804c4c39002135cb5cbc9ff4bb677): 59s
- T-5 (0b084549ee3eb5c4dae9477d33ff10be8a533084): 1m 3s
- T-4 (56f4d4a39cbdee23a5856037430ab627ce37ab22): 58.2s
- T-3 (7b77c5668a7ad785310d7c46010c2863935aaa9a): 1m 14s
- T-2 (4a5a4922e0b8793c8b01af6aa55915ba518bc9bf): 1m 6.9s
- T-1 (38f4d4753a7205b3ec242440b3d2a1bbd866131a): 59.9s
- **T (current commit): 24.6s**</details>
<!--delta:benchmark@{}-->

Patch:
diff --git a/package-lock.json b/package-lock.json
index 1e70fc6..d490cb6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,6 +23,7 @@
         "filter-obj": "^2.0.1",
         "find-up": "^5.0.0",
         "glob": "^7.1.6",
+        "is-builtin-module": "^3.1.0",
         "junk": "^3.1.0",
         "locate-path": "^6.0.0",
         "make-dir": "^3.1.0",
@@ -2077,7 +2078,6 @@
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
       "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
-      "dev": true,
       "engines": {
         "node": ">=6"
       },
@@ -6311,7 +6311,6 @@
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.1.0.tgz",
       "integrity": "sha512-OV7JjAgOTfAFJmHZLvpSTb4qi0nIILDV1gWPYDnDJUTNFM5aGlRAhk4QcT8i7TuAleeEV5Fdkqn3t4mS+Q11fg==",
-      "dev": true,
       "dependencies": {
         "builtin-modules": "^3.0.0"
       },
@@ -13383,8 +13382,7 @@
     "builtin-modules": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
-      "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
-      "dev": true
+      "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA=="
     },
     "cache-base": {
       "version": "1.0.1",
@@ -16549,7 +16547,6 @@
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.1.0.tgz",
       "integrity": "sha512-OV7JjAgOTfAFJmHZLvpSTb4qi0nIILDV1gWPYDnDJUTNFM5aGlRAhk4QcT8i7TuAleeEV5Fdkqn3t4mS+Q11fg==",
-      "dev": true,
       "requires": {
         "builtin-modules": "^3.0.0"
       }
diff --git a/package.json b/package.json
index 3bb31b2..c62297f 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
     "filter-obj": "^2.0.1",
     "find-up": "^5.0.0",
     "glob": "^7.1.6",
+    "is-builtin-module": "^3.1.0",
     "junk": "^3.1.0",
     "locate-path": "^6.0.0",
     "make-dir": "^3.1.0",
diff --git a/src/feature_flags.js b/src/feature_flags.js
index 5c86004..651dfba 100644
--- a/src/feature_flags.js
+++ b/src/feature_flags.js
@@ -5,6 +5,7 @@ const FLAGS = {
   buildGoSource: Boolean(env.NETLIFY_EXPERIMENTAL_BUILD_GO_SOURCE),
   buildRustSource: Boolean(env.NETLIFY_EXPERIMENTAL_BUILD_RUST_SOURCE),
   defaultEsModulesToEsbuild: Boolean(env.NETLIFY_EXPERIMENTAL_DEFAULT_ES_MODULES_TO_ESBUILD),
+  parseWithEsbuild: false,
 }
 
 const getFlags = (input = {}, flags = FLAGS) =>
diff --git a/src/main.js b/src/main.js
index fc4aba1..4b62594 100644
--- a/src/main.js
+++ b/src/main.js
@@ -27,7 +27,7 @@ const listFunctionsFiles = async function (relativeSrcFolders, { config, feature
     getPluginsModulesPath(srcFolders[0]),
   ])
   const listedFunctionsFiles = await Promise.all(
-    [...functions.values()].map((func) => getListedFunctionFiles(func, { pluginsModulesPath })),
+    [...functions.values()].map((func) => getListedFunctionFiles(func, { featureFlags, pluginsModulesPath })),
   )
 
   // TODO: switch to Array.flat() once we drop support for Node.js < 11.0.0
@@ -41,9 +41,10 @@ const getListedFunction = function ({ runtime, name, mainFile, extension }) {
 
 const getListedFunctionFiles = async function (
   { config, runtime, name, stat, mainFile, extension, srcPath, srcDir },
-  { pluginsModulesPath },
+  { featureFlags, pluginsModulesPath },
 ) {
   const srcFiles = await getSrcFiles({
+    featureFlags,
     runtime,
     stat,
     mainFile,
@@ -59,6 +60,7 @@ const getListedFunctionFiles = async function (
 const getSrcFiles = function ({
   bundler,
   config,
+  featureFlags,
   runtime,
   stat,
   mainFile,
@@ -77,6 +79,7 @@ const getSrcFiles = function ({
     bundler,
     config,
     extension,
+    featureFlags,
     srcPath,
     mainFile,
     srcDir,
diff --git a/src/node_dependencies/index.js b/src/node_dependencies/index.js
index cef1c8a..51e5a3b 100644
--- a/src/node_dependencies/index.js
+++ b/src/node_dependencies/index.js
@@ -4,6 +4,8 @@ const findUp = require('find-up')
 const { not: notJunk } = require('junk')
 const precinct = require('precinct')
 
+const { listImports } = require('../runtimes/node/list_imports')
+
 const { getPackageJson } = require('./package_json')
 const { resolvePathPreserveSymlinks } = require('./resolve')
 const { getExternalAndIgnoredModulesFromSpecialCases } = require('./special_cases')
@@ -23,10 +25,17 @@ const getPluginsModulesPath = (srcDir) => findUp(`${AUTO_PLUGINS_DIR}node_module
 // Retrieve the paths to the Node.js files to zip.
 // We only include the files actually needed by the function because AWS Lambda
 // has a size limit for the zipped file. It also makes cold starts faster.
-const listFilesUsingLegacyBundler = async function ({ srcPath, mainFile, srcDir, stat, pluginsModulesPath }) {
+const listFilesUsingLegacyBundler = async function ({
+  featureFlags,
+  srcPath,
+  mainFile,
+  srcDir,
+  stat,
+  pluginsModulesPath,
+}) {
   const [treeFiles, depFiles] = await Promise.all([
     getTreeFiles(srcPath, stat),
-    getDependencies(mainFile, srcDir, pluginsModulesPath),
+    getDependencies(mainFile, srcDir, pluginsModulesPath, featureFlags),
   ])
   const files = [...treeFiles, ...depFiles].map(normalize)
   const uniqueFiles = [...new Set(files)]
@@ -43,19 +52,26 @@ const isNotJunk = function (file) {
 }
 
 // Retrieve all the files recursively required by a Node.js file
-const getDependencies = async function (mainFile, srcDir, pluginsModulesPath) {
+const getDependencies = async function (mainFile, srcDir, pluginsModulesPath, featureFlags) {
   const packageJson = await getPackageJson(srcDir)
   const state = getNewCache()
 
   try {
-    return await getFileDependencies({ path: mainFile, packageJson, state, pluginsModulesPath })
+    return await getFileDependencies({ featureFlags, path: mainFile, packageJson, pluginsModulesPath, state })
   } catch (error) {
     error.message = `In file "${mainFile}"\n${error.message}`
     throw error
   }
 }
 
-const getFileDependencies = async function ({ path, packageJson, pluginsModulesPath, state, treeShakeNext }) {
+const getFileDependencies = async function ({
+  featureFlags,
+  path,
+  packageJson,
+  pluginsModulesPath,
+  state,
+  treeShakeNext,
+}) {
   if (state.localFiles.has(path)) {
     return []
   }
@@ -63,16 +79,21 @@ const getFileDependencies = async function ({ path, packageJson, pluginsModulesP
   state.localFiles.add(path)
 
   const basedir = dirname(path)
-  // This parses JavaScript in `path` to retrieve all the `require()` statements
-  // TODO: `precinct.paperwork()` uses `fs.readFileSync()` under the hood,
-  // but should use `fs.readFile()` instead
-  const dependencies = precinct.paperwork(path, { includeCore: false })
+  const dependencies = featureFlags.parseWithEsbuild
+    ? await listImports({ path })
+    : precinct.paperwork(path, { includeCore: false })
   const depsPaths = await Promise.all(
-    dependencies
-      .filter(Boolean)
-      .map((dependency) =>
-        getImportDependencies({ dependency, basedir, packageJson, state, treeShakeNext, pluginsModulesPath }),
-      ),
+    dependencies.filter(Boolean).map((dependency) =>
+      getImportDependencies({
+        dependency,
+        basedir,
+        featureFlags,
+        packageJson,
+        pluginsModulesPath,
+        state,
+        treeShakeNext,
+      }),
+    ),
   )
   // TODO: switch to Array.flat() once we drop support for Node.js < 11.0.0
   // eslint-disable-next-line unicorn/prefer-spread
@@ -82,20 +103,22 @@ const getFileDependencies = async function ({ path, packageJson, pluginsModulesP
 const getImportDependencies = function ({
   dependency,
   basedir,
+  featureFlags,
   packageJson,
+  pluginsModulesPath,
   state,
   treeShakeNext,
-  pluginsModulesPath,
 }) {
   const shouldTreeShakeNext = treeShakeNext || isNextOnNetlify(dependency)
   if (shouldTreeShake(dependency, shouldTreeShakeNext)) {
     return getTreeShakedDependencies({
       dependency,
       basedir,
+      featureFlags,
       packageJson,
+      pluginsModulesPath,
       state,
       treeShakeNext: shouldTreeShakeNext,
-      pluginsModulesPath,
     })
   }
 
@@ -110,13 +133,21 @@ const isNextOnNetlify = function (dependency) {
 const getTreeShakedDependencies = async function ({
   dependency,
   basedir,
+  featureFlags,
   packageJson,
+  pluginsModulesPath,
   state,
   treeShakeNext,
-  pluginsModulesPath,
 }) {
   const path = await resolvePathPreserveSymlinks(dependency, [basedir, pluginsModulesPath].filter(Boolean))
-  const depsPath = await getFileDependencies({ path, packageJson, state, treeShakeNext, pluginsModulesPath })
+  const depsPath = await getFileDependencies({
+    featureFlags,
+    path,
+    packageJson,
+    pluginsModulesPath,
+    state,
+    treeShakeNext,
+  })
   return [path, ...depsPath]
 }
 
diff --git a/src/runtimes/node/index.js b/src/runtimes/node/index.js
index 7eb80c2..64198ce 100644
--- a/src/runtimes/node/index.js
+++ b/src/runtimes/node/index.js
@@ -42,6 +42,7 @@ const zipFunction = async function ({
   config = {},
   destFolder,
   extension,
+  featureFlags,
   filename,
   mainFile,
   name,
@@ -49,7 +50,6 @@ const zipFunction = async function ({
   srcDir,
   srcPath,
   stat,
-  featureFlags,
 }) {
   const bundler = config.nodeBundler || (await getDefaultBundler({ extension, mainFile, featureFlags }))
   // If the file is a zip, we assume the function is bundled and ready to go.
@@ -67,6 +67,7 @@ const zipFunction = async function ({
       config,
       destFolder,
       extension,
+      featureFlags,
       filename,
       mainFile,
       pluginsModulesPath,
diff --git a/src/runtimes/node/list_imports.js b/src/runtimes/node/list_imports.js
new file mode 100644
index 0000000..8ea4fe0
--- /dev/null
+++ b/src/runtimes/node/list_imports.js
@@ -0,0 +1,51 @@
+const esbuild = require('@netlify/esbuild')
+const isBuiltinModule = require('is-builtin-module')
+const { tmpName } = require('tmp-promise')
+
+const { safeUnlink } = require('../../utils/fs')
+
+const getListImportsPlugin = ({ imports, path }) => ({
+  name: 'list-imports',
+  setup(build) {
+    build.onResolve({ filter: /.*/ }, (args) => {
+      const isEntryPoint = args.path === path
+      const isImport = !isEntryPoint && !isBuiltinModule(args.path)
+
+      if (isImport) {
+        imports.add(args.path)
+      }
+
+      return {
+        namespace: 'list-imports',
+        external: isImport,
+      }
+    })
+  },
+})
+
+const listImports = async ({ path }) => {
+  // We're not interested in the output that esbuild generates, we're just
+  // using it for its parsing capabilities in order to find import/require
+  // statements. However, if we don't give esbuild a path in `outfile`, it
+  // will pipe the output to stdout, which we also don't want. So we create
+  // a temporary file to serve as the esbuild output and then get rid of it
+  // when we're done.
+  const targetPath = await tmpName()
+  const imports = new Set()
+
+  try {
+    await esbuild.build({
+      entryPoints: [path],
+      bundle: true,
+      outfile: targetPath,
+      platform: 'node',
+      plugins: [getListImportsPlugin({ imports, path })],
+    })
+  } finally {
+    await safeUnlink(targetPath)
+  }
+
+  return [...imports]
+}
+
+module.exports = { listImports }
diff --git a/src/runtimes/node/src_files.js b/src/runtimes/node/src_files.js
index bfcd759..8df3adf 100644
--- a/src/runtimes/node/src_files.js
+++ b/src/runtimes/node/src_files.js
@@ -53,6 +53,7 @@ const getSrcFiles = async function ({ config, ...parameters }) {
 const getSrcFilesAndExternalModules = async function ({
   bundler,
   externalNodeModules = [],
+  featureFlags,
   includedFiles = [],
   includedFilesBasePath,
   mainFile,
@@ -64,7 +65,14 @@ const getSrcFilesAndExternalModules = async function ({
   const includedFilePaths = await getPathsOfIncludedFiles(includedFiles, includedFilesBasePath)
 
   if (bundler === JS_BUNDLER_ZISI) {
-    const paths = await listFilesUsingLegacyBundler({ srcPath, mainFile, srcDir, stat, pluginsModulesPath })
+    const paths = await listFilesUsingLegacyBundler({
+      featureFlags,
+      srcPath,
+      mainFile,
+      srcDir,
+      stat,
+      pluginsModulesPath,
+    })
 
     return {
       moduleNames: [],
diff --git a/src/runtimes/node/zip_zisi.js b/src/runtimes/node/zip_zisi.js
index b38fc29..3eead74 100644
--- a/src/runtimes/node/zip_zisi.js
+++ b/src/runtimes/node/zip_zisi.js
@@ -12,6 +12,7 @@ const zipZisi = async ({
   config,
   destFolder,
   extension,
+  featureFlags,
   filename,
   mainFile,
   pluginsModulesPath,
@@ -22,6 +23,7 @@ const zipZisi = async ({
   const { paths: srcFiles } = await getSrcFilesAndExternalModules({
     bundler: JS_BUNDLER_ZISI,
     extension,
+    featureFlags,
     includedFiles: config.includedFiles,
     includedFilesBasePath: config.includedFilesBasePath || basePath,
     mainFile,


Test patch:
diff --git a/tests/fixtures/local-node-module-deep-require/function.js b/tests/fixtures/local-node-module-deep-require/function.js
new file mode 100644
index 0000000..ddca32a
--- /dev/null
+++ b/tests/fixtures/local-node-module-deep-require/function.js
@@ -0,0 +1,4 @@
+const mock = require('@netlify/mock-package')
+const stack = require('@netlify/mock-package/stack')
+
+module.exports = { mock, stack }
diff --git a/tests/fixtures/local-node-module-deep-require/node_modules/@netlify/mock-package/index.js b/tests/fixtures/local-node-module-deep-require/node_modules/@netlify/mock-package/index.js
new file mode 100644
index 0000000..dc03a1a
--- /dev/null
+++ b/tests/fixtures/local-node-module-deep-require/node_modules/@netlify/mock-package/index.js
@@ -0,0 +1,3 @@
+const stack = require('./stack')
+
+module.exports = { stack }
diff --git a/tests/fixtures/local-node-module-deep-require/node_modules/@netlify/mock-package/package.json b/tests/fixtures/local-node-module-deep-require/node_modules/@netlify/mock-package/package.json
new file mode 100644
index 0000000..970c87d
--- /dev/null
+++ b/tests/fixtures/local-node-module-deep-require/node_modules/@netlify/mock-package/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "@netlify/mock-package",
+  "version": "1.0.0"
+}
diff --git a/tests/fixtures/local-node-module-deep-require/node_modules/@netlify/mock-package/stack.js b/tests/fixtures/local-node-module-deep-require/node_modules/@netlify/mock-package/stack.js
new file mode 100644
index 0000000..5c06561
--- /dev/null
+++ b/tests/fixtures/local-node-module-deep-require/node_modules/@netlify/mock-package/stack.js
@@ -0,0 +1,1 @@
+module.exports = 'jam'
diff --git a/tests/fixtures/local-node-module-deep-require/package.json b/tests/fixtures/local-node-module-deep-require/package.json
new file mode 100644
index 0000000..a848bab
--- /dev/null
+++ b/tests/fixtures/local-node-module-deep-require/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "local-node-module",
+  "version": "1.0.0",
+  "dependencies": {
+    "@netlify/mock-package": "1.0.0"
+  }
+}
diff --git a/tests/helpers/main.js b/tests/helpers/main.js
index 342bcf0..a27e928 100644
--- a/tests/helpers/main.js
+++ b/tests/helpers/main.js
@@ -3,10 +3,10 @@ const { env } = require('process')
 const { promisify } = require('util')
 
 const AdmZip = require('adm-zip')
-const precinct = require('precinct')
 const { dir: getTmpDir } = require('tmp-promise')
 
 const { zipFunctions } = require('../..')
+const { listImports } = require('../../src/runtimes/node/list_imports')
 const { ARCHIVE_FORMAT_ZIP } = require('../../src/utils/consts')
 
 const FIXTURES_DIR = join(__dirname, '..', 'fixtures')
@@ -77,8 +77,8 @@ const replaceUnzipPath = function ({ path }) {
 // Returns a list of paths included using `require` calls. Relative requires
 // will be traversed recursively up to a depth defined by `depth`. All the
 // required paths — relative or not — will be returned in a flattened array.
-const getRequires = function ({ depth = Number.POSITIVE_INFINITY, filePath }, currentDepth = 1) {
-  const requires = precinct.paperwork(filePath, { includeCore: false })
+const getRequires = async function ({ depth = Number.POSITIVE_INFINITY, filePath }, currentDepth = 1) {
+  const requires = await listImports({ path: filePath })
 
   if (currentDepth >= depth) {
     return requires
diff --git a/tests/main.js b/tests/main.js
index 7026b88..5759822 100644
--- a/tests/main.js
+++ b/tests/main.js
@@ -135,6 +135,29 @@ testBundlers('Can require node modules', [ESBUILD, ESBUILD_ZISI, DEFAULT], async
   await zipNode(t, 'local-node-module', { opts: { config: { '*': { nodeBundler: bundler } } } })
 })
 
+testBundlers('Can require deep paths in node modules', [ESBUILD, ESBUILD_ZISI, DEFAULT], async (bundler, t) => {
+  const { tmpDir } = await zipNode(t, 'local-node-module-deep-require', {
+    opts: { config: { '*': { nodeBundler: bundler } } },
+  })
+
+  // eslint-disable-next-line import/no-dynamic-require, node/global-require
+  const func = require(`${tmpDir}/function.js`)
+
+  t.deepEqual(func, { mock: { stack: 'jam' }, stack: 'jam' })
+
+  if (bundler === DEFAULT) {
+    // TO DO: Remove when `parseWithEsbuild` feature flag is decommissioned.
+    const { tmpDir: tmpDir2 } = await zipNode(t, 'local-node-module-deep-require', {
+      opts: { config: { '*': { nodeBundler: bundler } } },
+    })
+
+    // eslint-disable-next-line import/no-dynamic-require, node/global-require
+    const func2 = require(`${tmpDir2}/function.js`)
+
+    t.deepEqual(func2, { mock: { stack: 'jam' }, stack: 'jam' })
+  }
+})
+
 testBundlers('Can require scoped node modules', [ESBUILD, ESBUILD_ZISI, DEFAULT], async (bundler, t) => {
   await zipNode(t, 'node-module-scope', { opts: { config: { '*': { nodeBundler: bundler } } } })
 })
@@ -307,6 +330,11 @@ testBundlers(
 
 testBundlers('Can require local files deeply', [ESBUILD, ESBUILD_ZISI, DEFAULT], async (bundler, t) => {
   await zipNode(t, 'local-deep-require', { opts: { config: { '*': { nodeBundler: bundler } } } })
+
+  // TO DO: Remove when `parseWithEsbuild` feature flag is decommissioned.
+  await zipNode(t, 'local-deep-require', {
+    opts: { config: { '*': { nodeBundler: bundler } }, featureFlags: { parseWithEsbuild: true } },
+  })
 })
 
 testBundlers(
@@ -432,6 +460,11 @@ testBundlers(
 
 testBundlers('Works with many dependencies', [ESBUILD, ESBUILD_ZISI, DEFAULT], async (bundler, t) => {
   await zipNode(t, 'many-dependencies', { opts: { config: { '*': { nodeBundler: bundler } } } })
+
+  // TO DO: Remove when `parseWithEsbuild` feature flag is decommissioned.
+  await zipNode(t, 'many-dependencies', {
+    opts: { config: { '*': { nodeBundler: bundler } }, featureFlags: { parseWithEsbuild: true } },
+  })
 })
 
 testBundlers('Works with many function files', [ESBUILD, ESBUILD_ZISI, DEFAULT], async (bundler, t) => {