Repository: netlify/zip-it-and-ship-it (js)

Issue:
Adjust esbuild target to AWS_LAMBDA_JS_RUNTIME
**- Do you want to request a _feature_ or report a _bug_?**

Feature.

**- What is the current behavior?**

Currently, esbuild is configured to use `es2017` as the target. We should make it this configurable, and we should take the `AWS_LAMBDA_JS_RUNTIME` environment variable into account for this.

Pull request description:
feat: add nodeVersion config property
**- Summary**

Introduces a new configuration property called `nodeVersion`, which specifies the version of Node.js that esbuild will use as the compilation target.

It supports values like `10.x`, as well as the `nodejs10.x` format used by [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html).

The idea is for consumers of `zip-it-and-ship-it` to supply this configuration property based on the environment it's running on. For example, Netlify Build could pass along the value of the `AWS_LAMBDA_JS_RUNTIME` environment variable, whereas Netlify CLI could supply the local version of Node.js when serving bundled functions in `netlify dev`.

Closes #400.

**- Test plan**

Added one new test.

**- Description for the changelog**

feat: add nodeVersion config property

**- A picture of a cute animal (not mandatory but encouraged)**

![131138](https://user-images.githubusercontent.com/4162329/114999849-205c3c80-9e9a-11eb-9324-f43908f8e733.jpg)


Patch:
diff --git a/README.md b/README.md
index 3daf916..505dd5c 100644
--- a/README.md
+++ b/README.md
@@ -112,6 +112,18 @@ The following properties are accepted:
 
   When the value is `esbuild_zisi`, `esbuild` will be used with a fallback to `zisi` in case of an error.
 
+- `nodeVersion`
+
+  _Type_: `string`\
+  _Default value_: `12.x`
+
+  The version of Node.js to use as the compilation target. Possible values:
+
+  - `8.x` (or `nodejs8.x`)
+  - `10.x` (or `nodejs10.x`)
+  - `12.x` (or `nodejs12.x`)
+  - `14.x` (or `nodejs14.x`)
+
 #### parallelLimit
 
 _Type_: `number`\
diff --git a/src/runtimes/node/bundler.js b/src/runtimes/node/bundler.js
index 2dcb329..7fd2036 100644
--- a/src/runtimes/node/bundler.js
+++ b/src/runtimes/node/bundler.js
@@ -6,12 +6,14 @@ const { promisify } = require('util')
 const esbuild = require('esbuild')
 const semver = require('semver')
 
+const { getBundlerTarget } = require('./bundler_target')
 const { externalNativeModulesPlugin } = require('./native_modules/plugin')
 
 const pUnlink = promisify(fs.unlink)
 
 const bundleJsFile = async function ({
   additionalModulePaths,
+  config,
   destFilename,
   destFolder,
   externalModules = [],
@@ -40,6 +42,7 @@ const bundleJsFile = async function ({
 
   const externalizedModules = new Set()
   const plugins = [externalNativeModulesPlugin(externalizedModules)]
+  const nodeTarget = getBundlerTarget(config.nodeVersion)
 
   try {
     const data = await buildFunction({
@@ -52,7 +55,7 @@ const bundleJsFile = async function ({
       platform: 'node',
       plugins: supportsAsyncAPI ? plugins : [],
       resolveExtensions: ['.js', '.jsx', '.mjs', '.cjs', '.json'],
-      target: ['es2017'],
+      target: [nodeTarget],
     })
 
     return { bundlePath, cleanTempFiles, data, externalizedModules: [...externalizedModules] }
diff --git a/src/runtimes/node/bundler_target.js b/src/runtimes/node/bundler_target.js
new file mode 100644
index 0000000..40d9df3
--- /dev/null
+++ b/src/runtimes/node/bundler_target.js
@@ -0,0 +1,22 @@
+const DEFAULT_VERSION = 'node12'
+
+const versionMap = {
+  '8.x': 'node8',
+  '10.x': 'node10',
+  '12.x': 'node12',
+  '14.x': 'node14',
+}
+
+const getBundlerTarget = (suppliedVersion) => {
+  const version = normalizeVersion(suppliedVersion)
+
+  return versionMap[version] || DEFAULT_VERSION
+}
+
+const normalizeVersion = (version) => {
+  const match = version && version.match(/^nodejs(.*)$/)
+
+  return match ? match[1] : version
+}
+
+module.exports = { getBundlerTarget }
diff --git a/src/runtimes/node/zip_esbuild.js b/src/runtimes/node/zip_esbuild.js
index a62b9ee..1ea69f0 100644
--- a/src/runtimes/node/zip_esbuild.js
+++ b/src/runtimes/node/zip_esbuild.js
@@ -42,6 +42,7 @@ const zipEsbuild = async ({
   const { externalModules, ignoredModules } = await getExternalAndIgnoredModules({ config, srcDir })
   const { bundlePath, data, cleanTempFiles, externalizedModules } = await bundleJsFile({
     additionalModulePaths: pluginsModulesPath ? [pluginsModulesPath] : [],
+    config,
     destFilename: filename,
     destFolder,
     externalModules,


Test patch:
diff --git a/tests/fixtures/node-module-optional-catch-binding/function.js b/tests/fixtures/node-module-optional-catch-binding/function.js
new file mode 100644
index 0000000..390ba12
--- /dev/null
+++ b/tests/fixtures/node-module-optional-catch-binding/function.js
@@ -0,0 +1,12 @@
+const noop = () => {}
+
+module.exports.handler = () => {
+  try {
+    noop()
+    // eslint-disable-next-line node/no-unsupported-features/es-syntax
+  } catch {
+    // ¯\_(ツ)_/¯
+  }
+
+  return true
+}
diff --git a/tests/main.js b/tests/main.js
index ab7d91f..badcd74 100644
--- a/tests/main.js
+++ b/tests/main.js
@@ -900,6 +900,28 @@ testBundlers(
   },
 )
 
+test('Generates a bundle for the Node runtime version specified in the `nodeVersion` config property', async (t) => {
+  // Using the optional catch binding feature to assert that the bundle is
+  // respecting the Node version supplied.
+  // - in Node <10 we should see `try {} catch (e) {}`
+  // - in Node >= 10 we should see `try {} catch {}`
+  const { files: node8Files } = await zipNode(t, 'node-module-optional-catch-binding', {
+    opts: { archiveFormat: 'none', config: { '*': { nodeBundler: ESBUILD, nodeVersion: '8.x' } } },
+  })
+
+  const node8Function = await pReadFile(`${node8Files[0].path}/src/function.js`, 'utf8')
+
+  t.regex(node8Function, /catch \(\w+\) {/)
+
+  const { files: node12Files } = await zipNode(t, 'node-module-optional-catch-binding', {
+    opts: { archiveFormat: 'none', config: { '*': { nodeBundler: ESBUILD, nodeVersion: '12.x' } } },
+  })
+
+  const node12Function = await pReadFile(`${node12Files[0].path}/src/function.js`, 'utf8')
+
+  t.regex(node12Function, /catch {/)
+})
+
 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')