Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of calling the external Node.js plugin to handle this, which would require us to update the Node.js template each time, we just make a list of all dependencies in the package.json file and set those to external.

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