Skip to content
156 changes: 86 additions & 70 deletions aws_lambda_builders/workflows/nodejs_npm_esbuild/esbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
import logging
from pathlib import Path
from typing import Any, Dict, List
from typing import Any, Callable, Dict, List, Union

from aws_lambda_builders.actions import ActionFailedError
from aws_lambda_builders.workflows.nodejs_npm.utils import OSUtils
Expand Down Expand Up @@ -99,28 +99,7 @@ def run(self, args, cwd=None):
return out.decode("utf8").strip()


# The esbuild API flags are broken up into three forms (https://esbuild.github.io/api/):
# Multi-word arguments are expected to be passed down using snake case e.g. entry_points
# Boolean types (--minify)
SUPPORTED_ESBUILD_APIS_BOOLEAN = [
"minify",
"sourcemap",
]

# single value types (--target=es2020)
SUPPORTED_ESBUILD_APIS_SINGLE_VALUE = [
"target",
"format",
"main_fields",
"sources_content",
]

# Multi-value types (--external:axios --external:aws-sdk)
SUPPORTED_ESBUILD_APIS_MULTI_VALUE = [
"external",
"loader",
"out_extension",
]
NON_CONFIGURABLE_VALUES = {"bundle", "platform", "outdir"}


class EsbuildCommandBuilder:
Expand Down Expand Up @@ -154,15 +133,96 @@ def build_esbuild_args_from_config(self) -> "EsbuildCommandBuilder":
"""
args = []

args.extend(self._get_boolean_args())
args.extend(self._get_single_value_args())
args.extend(self._get_multi_value_args())
for config_key, config_value in self._bundler_config.items():
if config_key in NON_CONFIGURABLE_VALUES:
LOG.debug(
"'%s=%s' was not a used configuration since AWS Lambda Builders "
"sets these values for the code to be correctly consumed by AWS Lambda",
config_key,
config_value,
)
continue
if config_key == "entry_points":
# Entry points are a required parameter and are handled by the build_entry_points() method
continue
configuration_type_callback = self._get_config_type_callback(config_value)
LOG.debug("Configuring the parameter '%s=%s'", config_key, config_value)
args.extend(configuration_type_callback(config_key, config_value))

LOG.debug("Found the following args in the config: %s", str(args))

self._command.extend(args)
return self

def _get_config_type_callback(
self, config_value: Union[bool, str, list]
) -> Callable[[str, Union[bool, str, list]], List[str]]:
"""
Determines the type of the command and returns the corresponding
function to build out that command line argument type

:param config_value: Union[bool, str, list]
The configuration value configured through the options. The configuration should be one
of the supported types as defined by the esbuild API (https://esbuild.github.io/api/).
:return: Callable[[str, Union[bool, str, list]], List[str]]
Returns a function that the caller can use to turn the relevant
configuration into the correctly formatted command line argument.
"""
if isinstance(config_value, bool):
return self._create_boolean_config
elif isinstance(config_value, str):
return self._create_str_config
elif isinstance(config_value, list):
return self._create_list_config
raise EsbuildCommandError("Failed to determine the type of the configuration: %s", config_value)

def _create_boolean_config(self, config_key: str, config_value: bool) -> List[str]:
"""
Given boolean-type configuration, convert it to a string representation suitable for the esbuild API
Should be created in the form ([--config-key])

:param config_key: str
The configuration key to be used
:param config_value: bool
The configuration value to be used
:return: List[str]
List of resolved command line arguments to be appended to the builder
"""
if config_value is True:
return [f"--{self._convert_snake_to_kebab_case(config_key)}"]
return []

def _create_str_config(self, config_key: str, config_value: str) -> List[str]:
"""
Given string-type configuration, convert it to a string representation suitable for the esbuild API
Should be created in the form ([--config-key=config_value])

:param config_key: str
The configuration key to be used
:param config_value: List[str]
The configuration value to be used
:return: List[str]
List of resolved command line arguments to be appended to the builder
"""
return [f"--{self._convert_snake_to_kebab_case(config_key)}={config_value}"]

def _create_list_config(self, config_key: str, config_value: List[str]) -> List[str]:
"""
Given list-type configuration, convert it to a string representation suitable for the esbuild API
Should be created in the form ([--config-key:config_value_a, --config_key:config_value_b])

:param config_key: str
The configuration key to be used
:param config_value: List[str]
The configuration value to be used
:return: List[str]
List of resolved command line arguments to be appended to the builder
"""
args = []
for config_item in config_value:
args.append(f"--{self._convert_snake_to_kebab_case(config_key)}:{config_item}")
return args

def build_entry_points(self) -> "EsbuildCommandBuilder":
"""
Build the entry points to the command
Expand Down Expand Up @@ -227,50 +287,6 @@ def build_with_no_dependencies(self) -> "EsbuildCommandBuilder":
self._command.extend(args)
return self

def _get_boolean_args(self) -> List[str]:
"""
Get a list of all the boolean value flag types (e.g. --minify)

:rtype: List[str]
:return: Arguments to be appended to the command list
"""
args = []
for param in SUPPORTED_ESBUILD_APIS_BOOLEAN:
if param in self._bundler_config and self._bundler_config[param] is True:
args.append(f"--{self._convert_snake_to_kebab_case(param)}")
return args

def _get_single_value_args(self) -> List[str]:
"""
Get a list of all the single value flag types (e.g. --target=es2020)

:rtype: List[str]
:return: Arguments to be appended to the command list
"""
args = []
for param in SUPPORTED_ESBUILD_APIS_SINGLE_VALUE:
if param in self._bundler_config:
value = self._bundler_config.get(param)
args.append(f"--{self._convert_snake_to_kebab_case(param)}={value}")
return args

def _get_multi_value_args(self) -> List[str]:
"""
Get a list of all the multi-value flag types (e.g. --external:aws-sdk)

:rtype: List[str]
:return: Arguments to be appended to the command list
"""
args = []
for param in SUPPORTED_ESBUILD_APIS_MULTI_VALUE:
if param in self._bundler_config:
values = self._bundler_config.get(param)
if not isinstance(values, list):
raise EsbuildCommandError(f"Invalid type for property {param}, must be a dict.")
for param_item in values:
args.append(f"--{self._convert_snake_to_kebab_case(param)}:{param_item}")
return args

def _get_explicit_file_type(self, entry_point, entry_path):
"""
Get an entry point with an explicit .ts or .js suffix.
Expand Down
14 changes: 7 additions & 7 deletions tests/unit/workflows/nodejs_npm_esbuild/test_esbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,19 +209,19 @@ def test_builds_args_from_config(self, osutils_mock):
.get_command()
)
self.assertEqual(
args,
[
set(args),
{
"--minify",
"--target=node14",
"--format=esm",
"--main-fields=module,main",
"--sources-content=false",
"--external:aws-sdk",
"--external:axios",
"--loader:.proto=text",
"--loader:.json=js",
"--out-extension:.js=.mjs",
],
"--sources-content=false",
},
)

@patch("aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils")
Expand All @@ -235,8 +235,8 @@ def test_combined_builder_with_dependencies(self, osutils_mock):
.get_command()
)
self.assertEqual(
args,
[
set(args),
{
"x.js",
"--bundle",
"--platform=node",
Expand All @@ -246,7 +246,7 @@ def test_combined_builder_with_dependencies(self, osutils_mock):
"--format=esm",
"--loader:.proto=text",
"--loader:.json=js",
],
},
)

@parameterized.expand(
Expand Down