Skip to content

Commit 8a30554

Browse files
feat: add --external and --loader support for nodejs_npm_esbuild workflow (#353)
1 parent ef19e9f commit 8a30554

File tree

15 files changed

+259
-19
lines changed

15 files changed

+259
-19
lines changed

aws_lambda_builders/workflows/nodejs_npm/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,6 @@ def is_windows(self):
5353
def parse_json(self, path):
5454
with open(path) as json_file:
5555
return json.load(json_file)
56+
57+
def check_output(self, path):
58+
return subprocess.check_output(["node", path])

aws_lambda_builders/workflows/nodejs_npm_esbuild/actions.py

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ def __init__(
5454
5555
:type skip_deps: bool
5656
:param skip_deps: if dependencies should be omitted from bundling
57+
58+
:type bundler_config: Dict[str,Any]
59+
:param bundler_config: the bundler configuration
5760
"""
5861
super(EsbuildBundleAction, self).__init__()
5962
self.scratch_dir = scratch_dir
@@ -71,38 +74,29 @@ def execute(self):
7174
:raises lambda_builders.actions.ActionFailedError: when esbuild packaging fails
7275
"""
7376

74-
if self.ENTRY_POINTS not in self.bundler_config:
75-
raise ActionFailedError(f"{self.ENTRY_POINTS} not set ({self.bundler_config})")
76-
77-
entry_points = self.bundler_config[self.ENTRY_POINTS]
78-
79-
if not isinstance(entry_points, list):
80-
raise ActionFailedError(f"{self.ENTRY_POINTS} must be a list ({self.bundler_config})")
81-
82-
if not entry_points:
83-
raise ActionFailedError(f"{self.ENTRY_POINTS} must not be empty ({self.bundler_config})")
84-
85-
entry_paths = [self.osutils.joinpath(self.scratch_dir, entry_point) for entry_point in entry_points]
86-
87-
LOG.debug("NODEJS building %s using esbuild to %s", entry_paths, self.artifacts_dir)
88-
89-
explicit_entry_points = []
90-
for entry_path, entry_point in zip(entry_paths, entry_points):
91-
explicit_entry_points.append(self._get_explicit_file_type(entry_point, entry_path))
77+
explicit_entry_points = self._construct_esbuild_entry_points()
9278

9379
args = explicit_entry_points + ["--bundle", "--platform=node", "--format=cjs"]
9480
minify = self.bundler_config.get("minify", True)
9581
sourcemap = self.bundler_config.get("sourcemap", True)
9682
target = self.bundler_config.get("target", "es2020")
83+
external = self.bundler_config.get("external", [])
84+
loader = self.bundler_config.get("loader", [])
9785
if minify:
9886
args.append("--minify")
9987
if sourcemap:
10088
args.append("--sourcemap")
89+
if external:
90+
args.extend(map(lambda x: f"--external:{x}", external))
91+
if loader:
92+
args.extend(map(lambda x: f"--loader:{x}", loader))
93+
10194
args.append("--target={}".format(target))
10295
args.append("--outdir={}".format(self.artifacts_dir))
10396

10497
if self.skip_deps:
10598
LOG.info("Running custom esbuild using Node.js")
99+
# Don't pass externals because the esbuild.js template makes everything external
106100
script = EsbuildBundleAction._get_node_esbuild_template(
107101
explicit_entry_points, target, self.artifacts_dir, minify, sourcemap
108102
)
@@ -132,6 +126,30 @@ def _run_external_esbuild_in_nodejs(self, script):
132126
except EsbuildExecutionError as ex:
133127
raise ActionFailedError(str(ex))
134128

129+
def _construct_esbuild_entry_points(self):
130+
"""
131+
Construct the list of explicit entry points
132+
"""
133+
if self.ENTRY_POINTS not in self.bundler_config:
134+
raise ActionFailedError(f"{self.ENTRY_POINTS} not set ({self.bundler_config})")
135+
136+
entry_points = self.bundler_config[self.ENTRY_POINTS]
137+
138+
if not isinstance(entry_points, list):
139+
raise ActionFailedError(f"{self.ENTRY_POINTS} must be a list ({self.bundler_config})")
140+
141+
if not entry_points:
142+
raise ActionFailedError(f"{self.ENTRY_POINTS} must not be empty ({self.bundler_config})")
143+
144+
entry_paths = [self.osutils.joinpath(self.scratch_dir, entry_point) for entry_point in entry_points]
145+
146+
LOG.debug("NODEJS building %s using esbuild to %s", entry_paths, self.artifacts_dir)
147+
148+
explicit_entry_points = []
149+
for entry_path, entry_point in zip(entry_paths, entry_points):
150+
explicit_entry_points.append(self._get_explicit_file_type(entry_point, entry_path))
151+
return explicit_entry_points
152+
135153
@staticmethod
136154
def _get_node_esbuild_template(entry_points, target, out_dir, minify, sourcemap):
137155
"""

tests/integration/workflows/nodejs_npm_esbuild/test_nodejs_npm_with_esbuild.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,66 @@ def test_builds_project_without_combine_dependencies(self):
287287
expected_dependencies_files = {"node_modules"}
288288
output_dependencies_files = set(os.listdir(os.path.join(self.dependencies_dir)))
289289
self.assertNotIn(expected_dependencies_files, output_dependencies_files)
290+
291+
def test_builds_javascript_project_with_external(self):
292+
source_dir = os.path.join(self.TEST_DATA_FOLDER, "with-deps-esbuild-externals")
293+
294+
options = {"entry_points": ["included.js"], "external": ["minimal-request-promise"]}
295+
296+
self.builder.build(
297+
source_dir,
298+
self.artifacts_dir,
299+
self.scratch_dir,
300+
os.path.join(source_dir, "package.json"),
301+
runtime=self.runtime,
302+
options=options,
303+
experimental_flags=[EXPERIMENTAL_FLAG_ESBUILD],
304+
)
305+
306+
expected_files = {"included.js", "included.js.map"}
307+
output_files = set(os.listdir(self.artifacts_dir))
308+
self.assertEqual(expected_files, output_files)
309+
with open(str(os.path.join(self.artifacts_dir, "included.js"))) as f:
310+
js_file = f.read()
311+
# Check that the module has been require() instead of bundled
312+
self.assertIn('require("minimal-request-promise")', js_file)
313+
314+
def test_builds_javascript_project_with_loader(self):
315+
osutils = OSUtils()
316+
source_dir = os.path.join(self.TEST_DATA_FOLDER, "no-deps-esbuild-loader")
317+
318+
options = {"entry_points": ["included.js"], "loader": [".reference=json"]}
319+
320+
self.builder.build(
321+
source_dir,
322+
self.artifacts_dir,
323+
self.scratch_dir,
324+
os.path.join(source_dir, "package.json"),
325+
runtime=self.runtime,
326+
options=options,
327+
experimental_flags=[EXPERIMENTAL_FLAG_ESBUILD],
328+
)
329+
330+
expected_files = {"included.js", "included.js.map"}
331+
output_files = set(os.listdir(self.artifacts_dir))
332+
self.assertEqual(expected_files, output_files)
333+
334+
included_js_path = os.path.join(self.artifacts_dir, "included.js")
335+
336+
# check that the .reference file is correctly bundled as code by running the result
337+
self.assertEqual(
338+
osutils.check_output(included_js_path),
339+
str.encode(
340+
"===\n"
341+
"The Muses\n"
342+
"===\n"
343+
"\n"
344+
"\tcalliope: eloquence and heroic poetry\n"
345+
"\terato: lyric or erotic poetry\n"
346+
"\tmelpomene: tragedy\n"
347+
"\tpolymnia: sacred poetry\n"
348+
"\tterpsichore: dance\n"
349+
"\tthalia: comedy\n"
350+
"\turania: astronomy and astrology"
351+
),
352+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package-lock.json
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
//excluded
2+
const x = 1;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//included
2+
import { muses } from './reference.reference';
3+
4+
process.stdout.write("===\nThe Muses\n===\n\n");
5+
process.stdout.write(muses.map(muse => `\t${muse.name}: ${muse.description}`).join("\n"));
6+
7+
const x = 1;
8+
module.exports = x;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "nodeps-esbuild",
3+
"version": "1.0.0",
4+
"description": "",
5+
"keywords": [],
6+
"author": "",
7+
"license": "APACHE2.0",
8+
"devDependencies": {
9+
"esbuild": "^0.14.36"
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"muses": [
3+
{ "name": "calliope", "description": "eloquence and heroic poetry" },
4+
{ "name": "erato", "description": "lyric or erotic poetry" },
5+
{ "name": "melpomene", "description": "tragedy" },
6+
{ "name": "polymnia", "description": "sacred poetry" },
7+
{ "name": "terpsichore", "description": "dance" },
8+
{ "name": "thalia", "description": "comedy" },
9+
{ "name": "urania", "description": "astronomy and astrology" }
10+
]
11+
}

tests/integration/workflows/nodejs_npm_esbuild/testdata/no-deps-esbuild/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@
44
"description": "",
55
"keywords": [],
66
"author": "",
7-
"license": "APACHE2.0"
7+
"license": "APACHE2.0",
8+
"devDependencies": {
9+
"esbuild": "^0.14.36"
10+
}
811
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
//excluded
2+
const x = 1;

0 commit comments

Comments
 (0)