Skip to content

Commit

Permalink
feat: Esbuild Generic Command Builder (#372)
Browse files Browse the repository at this point in the history
* Cleanup esbuild actions, remove external node action

* Update existing tests

* Black reformat

* Add type hints and docstrings

* Add unit tests

* Remove unused import

* Update variable naming, add docstring, add format option
  • Loading branch information
mildaniel authored Jul 8, 2022
1 parent 23b71aa commit 64f478f
Show file tree
Hide file tree
Showing 10 changed files with 528 additions and 468 deletions.
194 changes: 42 additions & 152 deletions aws_lambda_builders/workflows/nodejs_npm_esbuild/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
Actions specific to the esbuild bundler
"""
import logging
from tempfile import NamedTemporaryFile

from pathlib import Path
from typing import Any, Dict

from aws_lambda_builders.actions import BaseAction, Purpose, ActionFailedError
from .esbuild import EsbuildExecutionError
from aws_lambda_builders.workflows.nodejs_npm.utils import OSUtils
from aws_lambda_builders.workflows.nodejs_npm_esbuild.esbuild import EsbuildCommandBuilder, SubprocessEsbuild
from aws_lambda_builders.workflows.nodejs_npm_esbuild.exceptions import EsbuildExecutionError

LOG = logging.getLogger(__name__)

EXTERNAL_KEY = "external"


class EsbuildBundleAction(BaseAction):

Expand All @@ -23,16 +25,14 @@ class EsbuildBundleAction(BaseAction):
DESCRIPTION = "Packaging source using Esbuild"
PURPOSE = Purpose.COPY_SOURCE

ENTRY_POINTS = "entry_points"

def __init__(
self,
scratch_dir,
artifacts_dir,
bundler_config,
osutils,
subprocess_esbuild,
subprocess_nodejs=None,
scratch_dir: str,
artifacts_dir: str,
bundler_config: Dict[str, Any],
osutils: OSUtils,
subprocess_esbuild: SubprocessEsbuild,
manifest: str,
skip_deps=False,
):
"""
Expand All @@ -49,168 +49,58 @@ def __init__(
:type subprocess_esbuild: aws_lambda_builders.workflows.nodejs_npm_esbuild.esbuild.SubprocessEsbuild
:param subprocess_esbuild: An instance of the Esbuild process wrapper
:type subprocess_nodejs: aws_lambda_builders.workflows.nodejs_npm_esbuild.node.SubprocessNodejs
:param subprocess_nodejs: An instance of the nodejs process wrapper
:type skip_deps: bool
:param skip_deps: if dependencies should be omitted from bundling
:type bundler_config: Dict[str,Any]
:param bundler_config: the bundler configuration
:type manifest: str
:param manifest: path to package.json file contents to read
"""
super(EsbuildBundleAction, self).__init__()
self.scratch_dir = scratch_dir
self.artifacts_dir = artifacts_dir
self.bundler_config = bundler_config
self.osutils = osutils
self.subprocess_esbuild = subprocess_esbuild
self.skip_deps = skip_deps
self.subprocess_nodejs = subprocess_nodejs
self._scratch_dir = scratch_dir
self._artifacts_dir = artifacts_dir
self._bundler_config = bundler_config
self._osutils = osutils
self._subprocess_esbuild = subprocess_esbuild
self._skip_deps = skip_deps
self._manifest = manifest

def execute(self):
def execute(self) -> None:
"""
Runs the action.
:raises lambda_builders.actions.ActionFailedError: when esbuild packaging fails
"""
esbuild_command = EsbuildCommandBuilder(
self._scratch_dir, self._artifacts_dir, self._bundler_config, self._osutils, self._manifest
)

explicit_entry_points = self._construct_esbuild_entry_points()

args = explicit_entry_points + ["--bundle", "--platform=node", "--format=cjs"]
minify = self.bundler_config.get("minify", True)
sourcemap = self.bundler_config.get("sourcemap", True)
target = self.bundler_config.get("target", "es2020")
external = self.bundler_config.get("external", [])
loader = self.bundler_config.get("loader", [])
if minify:
args.append("--minify")
if sourcemap:
args.append("--sourcemap")
if external:
args.extend(map(lambda x: f"--external:{x}", external))
if loader:
args.extend(map(lambda x: f"--loader:{x}", loader))

args.append("--target={}".format(target))
args.append("--outdir={}".format(self.artifacts_dir))

if self.skip_deps:
LOG.info("Running custom esbuild using Node.js")
# Don't pass externals because the esbuild.js template makes everything external
script = EsbuildBundleAction._get_node_esbuild_template(
explicit_entry_points, target, self.artifacts_dir, minify, sourcemap
)
self._run_external_esbuild_in_nodejs(script)
return
if self._should_bundle_deps_externally():
esbuild_command.build_with_no_dependencies()
if EXTERNAL_KEY in self._bundler_config:
# Already marking everything as external,
# shouldn't attempt to do it again when building args from config
self._bundler_config.pop(EXTERNAL_KEY)

args = (
esbuild_command.build_entry_points().build_default_values().build_esbuild_args_from_config().get_command()
)

try:
self.subprocess_esbuild.run(args, cwd=self.scratch_dir)
self._subprocess_esbuild.run(args, cwd=self._scratch_dir)
except EsbuildExecutionError as ex:
raise ActionFailedError(str(ex))

def _run_external_esbuild_in_nodejs(self, script):
"""
Run esbuild in a separate process through Node.js
Workaround for https://github.com/evanw/esbuild/issues/1958
:type script: str
:param script: Node.js script to execute
:raises lambda_builders.actions.ActionFailedError: when esbuild packaging fails
"""
with NamedTemporaryFile(dir=self.scratch_dir, mode="w") as tmp:
tmp.write(script)
tmp.flush()
try:
self.subprocess_nodejs.run([tmp.name], cwd=self.scratch_dir)
except EsbuildExecutionError as ex:
raise ActionFailedError(str(ex))

def _construct_esbuild_entry_points(self):
"""
Construct the list of explicit entry points
"""
if self.ENTRY_POINTS not in self.bundler_config:
raise ActionFailedError(f"{self.ENTRY_POINTS} not set ({self.bundler_config})")

entry_points = self.bundler_config[self.ENTRY_POINTS]

if not isinstance(entry_points, list):
raise ActionFailedError(f"{self.ENTRY_POINTS} must be a list ({self.bundler_config})")

if not entry_points:
raise ActionFailedError(f"{self.ENTRY_POINTS} must not be empty ({self.bundler_config})")

entry_paths = [self.osutils.joinpath(self.scratch_dir, entry_point) for entry_point in entry_points]

LOG.debug("NODEJS building %s using esbuild to %s", entry_paths, self.artifacts_dir)

explicit_entry_points = []
for entry_path, entry_point in zip(entry_paths, entry_points):
explicit_entry_points.append(self._get_explicit_file_type(entry_point, entry_path))
return explicit_entry_points

@staticmethod
def _get_node_esbuild_template(entry_points, target, out_dir, minify, sourcemap):
def _should_bundle_deps_externally(self) -> bool:
"""
Get the esbuild nodejs plugin template
:type entry_points: List[str]
:param entry_points: list of entry points
:type target: str
:param target: target version
:type out_dir: str
:param out_dir: output directory to bundle into
:type minify: bool
:param minify: if bundled code should be minified
:type sourcemap: bool
:param sourcemap: if esbuild should produce a sourcemap
Checks if all dependencies should be marked as external and not bundled with source code
:rtype: str
:return: formatted template
:rtype: boolean
:return: True if all dependencies should be marked as external
"""
curr_dir = Path(__file__).resolve().parent
with open(str(Path(curr_dir, "esbuild-plugin.js.template")), "r") as f:
input_str = f.read()
result = input_str.format(
target=target,
minify="true" if minify else "false",
sourcemap="true" if sourcemap else "false",
out_dir=repr(out_dir),
entry_points=entry_points,
)
return result

def _get_explicit_file_type(self, entry_point, entry_path):
"""
Get an entry point with an explicit .ts or .js suffix.
:type entry_point: str
:param entry_point: path to entry file from code uri
:type entry_path: str
:param entry_path: full path of entry file
:rtype: str
:return: entry point with appropriate file extension
:raises lambda_builders.actions.ActionFailedError: when esbuild packaging fails
"""
if Path(entry_point).suffix:
if self.osutils.file_exists(entry_path):
return entry_point
raise ActionFailedError("entry point {} does not exist".format(entry_path))

for ext in [".ts", ".js"]:
entry_path_with_ext = entry_path + ext
if self.osutils.file_exists(entry_path_with_ext):
return entry_point + ext

raise ActionFailedError("entry point {} does not exist".format(entry_path))
return self._skip_deps or "./node_modules/*" in self._bundler_config.get(EXTERNAL_KEY, [])


class EsbuildCheckVersionAction(BaseAction):
Expand Down

This file was deleted.

Loading

0 comments on commit 64f478f

Please sign in to comment.