Skip to content

Commit cafa4c3

Browse files
simalexansriram-mv
authored andcommitted
Extend Node.js NPM support for .npmrc configuration (#53)
* adding support for .npmrc * updating design document * update design document * correcting to OSError * minor cleanup * adding a test when .npmrc doesn't exist * renaming the global cleanup name to specific npmrc cleanup
1 parent b811288 commit cafa4c3

File tree

12 files changed

+275
-9
lines changed

12 files changed

+275
-9
lines changed

aws_lambda_builders/workflows/nodejs_npm/DESIGN.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,8 @@ the local `node_modules` subdirectory. This has to be executed in the directory
122122
a clean copy of the source files.
123123

124124
Note that NPM can be configured to use proxies or local company repositories using
125-
a local file, `.npmrc`. The packaging process from step 1 normally excludes this file, so it may
126-
need to be copied additionally before dependency installation, and then removed.
127-
_(out of scope for the current version)_
125+
a local file, `.npmrc`. The packaging process from step 1 normally excludes this file, so it needs
126+
to be copied before dependency installation, and then removed.
128127

129128
Some users may want to exclude optional dependencies, or even include development dependencies.
130129
To avoid incompatible flags in the `sam` CLI, the packager should allow users to specify

aws_lambda_builders/workflows/nodejs_npm/actions.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,87 @@ def execute(self):
111111

112112
except NpmExecutionError as ex:
113113
raise ActionFailedError(str(ex))
114+
115+
class NodejsNpmrcCopyAction(BaseAction):
116+
117+
"""
118+
A Lambda Builder Action that copies NPM config file .npmrc
119+
"""
120+
121+
NAME = 'CopyNpmrc'
122+
DESCRIPTION = "Copying configuration from .npmrc"
123+
PURPOSE = Purpose.COPY_SOURCE
124+
125+
def __init__(self, artifacts_dir, source_dir, osutils):
126+
"""
127+
:type artifacts_dir: str
128+
:param artifacts_dir: an existing (writable) directory with project source files.
129+
Dependencies will be installed in this directory.
130+
131+
:type source_dir: str
132+
:param source_dir: directory containing project source files.
133+
134+
:type osutils: aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils
135+
:param osutils: An instance of OS Utilities for file manipulation
136+
"""
137+
138+
super(NodejsNpmrcCopyAction, self).__init__()
139+
self.artifacts_dir = artifacts_dir
140+
self.source_dir = source_dir
141+
self.osutils = osutils
142+
143+
def execute(self):
144+
"""
145+
Runs the action.
146+
147+
:raises lambda_builders.actions.ActionFailedError: when .npmrc copying fails
148+
"""
149+
150+
try:
151+
npmrc_path = self.osutils.joinpath(self.source_dir, ".npmrc")
152+
if self.osutils.file_exists(npmrc_path):
153+
LOG.debug(".npmrc copying in: %s", self.artifacts_dir)
154+
self.osutils.copy_file(npmrc_path, self.artifacts_dir)
155+
156+
except OSError as ex:
157+
raise ActionFailedError(str(ex))
158+
159+
class NodejsNpmrcCleanUpAction(BaseAction):
160+
161+
"""
162+
A Lambda Builder Action that cleans NPM config file .npmrc
163+
"""
164+
165+
NAME = 'CleanUpNpmrc'
166+
DESCRIPTION = "Cleans artifacts dir"
167+
PURPOSE = Purpose.COPY_SOURCE
168+
169+
def __init__(self, artifacts_dir, osutils):
170+
"""
171+
:type artifacts_dir: str
172+
:param artifacts_dir: an existing (writable) directory with project source files.
173+
Dependencies will be installed in this directory.
174+
175+
:type osutils: aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils
176+
:param osutils: An instance of OS Utilities for file manipulation
177+
"""
178+
179+
super(NodejsNpmrcCleanUpAction, self).__init__()
180+
self.artifacts_dir = artifacts_dir
181+
self.osutils = osutils
182+
183+
def execute(self):
184+
"""
185+
Runs the action.
186+
187+
:raises lambda_builders.actions.ActionFailedError: when .npmrc copying fails
188+
"""
189+
190+
try:
191+
npmrc_path = self.osutils.joinpath(self.artifacts_dir, ".npmrc")
192+
if self.osutils.file_exists(npmrc_path):
193+
LOG.debug(".npmrc cleanup in: %s", self.artifacts_dir)
194+
self.osutils.remove_file(npmrc_path)
195+
196+
except OSError as ex:
197+
raise ActionFailedError(str(ex))

aws_lambda_builders/workflows/nodejs_npm/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import platform
77
import tarfile
88
import subprocess
9+
import shutil
910

1011

1112
class OSUtils(object):
@@ -15,10 +16,16 @@ class OSUtils(object):
1516
unit test actions in memory
1617
"""
1718

19+
def copy_file(self, file_path, destination_path):
20+
return shutil.copy2(file_path, destination_path)
21+
1822
def extract_tarfile(self, tarfile_path, unpack_dir):
1923
with tarfile.open(tarfile_path, 'r:*') as tar:
2024
tar.extractall(unpack_dir)
2125

26+
def file_exists(self, filename):
27+
return os.path.isfile(filename)
28+
2229
def joinpath(self, *args):
2330
return os.path.join(*args)
2431

@@ -33,6 +40,9 @@ def pipe(self):
3340
def dirname(self, path):
3441
return os.path.dirname(path)
3542

43+
def remove_file(self, filename):
44+
return os.remove(filename)
45+
3646
def abspath(self, path):
3747
return os.path.abspath(path)
3848

aws_lambda_builders/workflows/nodejs_npm/workflow.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from aws_lambda_builders.workflow import BaseWorkflow, Capability
66
from aws_lambda_builders.actions import CopySourceAction
7-
from .actions import NodejsNpmPackAction, NodejsNpmInstallAction
7+
from .actions import NodejsNpmPackAction, NodejsNpmInstallAction, NodejsNpmrcCopyAction, NodejsNpmrcCleanUpAction
88
from .utils import OSUtils
99
from .npm import SubprocessNpm
1010

@@ -55,8 +55,13 @@ def __init__(self,
5555

5656
npm_install = NodejsNpmInstallAction(artifacts_dir,
5757
subprocess_npm=subprocess_npm)
58+
59+
npm_copy_npmrc = NodejsNpmrcCopyAction(tar_package_dir, source_dir, osutils=osutils)
60+
5861
self.actions = [
5962
npm_pack,
63+
npm_copy_npmrc,
6064
CopySourceAction(tar_package_dir, artifacts_dir, excludes=self.EXCLUDED_FILES),
6165
npm_install,
66+
NodejsNpmrcCleanUpAction(artifacts_dir, osutils=osutils)
6267
]

tests/functional/workflows/nodejs_npm/test_utils.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,58 @@ def setUp(self):
1414

1515
self.osutils = utils.OSUtils()
1616

17+
def test_copy_file_copies_existing_file_into_a_dir(self):
18+
19+
test_file = os.path.join(os.path.dirname(__file__), "test_data", "test.tgz")
20+
21+
test_dir = tempfile.mkdtemp()
22+
23+
self.osutils.copy_file(test_file, test_dir)
24+
25+
output_files = set(os.listdir(test_dir))
26+
27+
shutil.rmtree(test_dir)
28+
29+
self.assertEqual({"test.tgz"}, output_files)
30+
31+
def test_copy_file_copies_existing_file_into_a_file(self):
32+
33+
test_file = os.path.join(os.path.dirname(__file__), "test_data", "test.tgz")
34+
35+
test_dir = tempfile.mkdtemp()
36+
37+
self.osutils.copy_file(test_file, os.path.join(test_dir, "copied_test.tgz"))
38+
39+
output_files = set(os.listdir(test_dir))
40+
41+
shutil.rmtree(test_dir)
42+
43+
self.assertEqual({"copied_test.tgz"}, output_files)
44+
45+
def test_remove_file_removes_existing_file(self):
46+
47+
test_file = os.path.join(os.path.dirname(__file__), "test_data", "test.tgz")
48+
49+
test_dir = tempfile.mkdtemp()
50+
51+
copied_file = os.path.join(test_dir, "copied_test.tgz")
52+
53+
shutil.copy(test_file, copied_file)
54+
55+
self.osutils.remove_file(copied_file)
56+
57+
self.assertFalse(os.path.isfile(copied_file))
58+
59+
def test_file_exists_checking_if_file_exists_in_a_dir(self):
60+
61+
existing_file = os.path.join(os.path.dirname(__file__), "test_data", "test.tgz")
62+
63+
nonexisting_file = os.path.join(os.path.dirname(__file__), "test_data", "nonexisting.tgz")
64+
65+
self.assertTrue(self.osutils.file_exists(existing_file))
66+
67+
self.assertFalse(self.osutils.file_exists(nonexisting_file))
68+
1769
def test_extract_tarfile_unpacks_a_tar(self):
1870

1971
test_tar = os.path.join(os.path.dirname(__file__), "test_data", "test.tgz")

tests/integration/workflows/nodejs_npm/test_nodejs_npm.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,22 @@ def test_builds_project_with_remote_dependencies(self):
5656
output_modules = set(os.listdir(os.path.join(self.artifacts_dir, "node_modules")))
5757
self.assertEquals(expected_modules, output_modules)
5858

59+
def test_builds_project_with_npmrc(self):
60+
source_dir = os.path.join(self.TEST_DATA_FOLDER, "npmrc")
61+
62+
self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir,
63+
os.path.join(source_dir, "package.json"),
64+
runtime=self.runtime)
65+
66+
expected_files = {"package.json", "included.js", "node_modules"}
67+
output_files = set(os.listdir(self.artifacts_dir))
68+
69+
self.assertEquals(expected_files, output_files)
70+
71+
expected_modules = {"fake-http-request"}
72+
output_modules = set(os.listdir(os.path.join(self.artifacts_dir, "node_modules")))
73+
self.assertEquals(expected_modules, output_modules)
74+
5975
def test_fails_if_npm_cannot_resolve_dependencies(self):
6076

6177
source_dir = os.path.join(self.TEST_DATA_FOLDER, "broken-deps")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
optional=false
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
//included
2+
const x = 1;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "npmdeps",
3+
"version": "1.0.0",
4+
"description": "",
5+
"files": ["included.js"],
6+
"keywords": [],
7+
"author": "",
8+
"license": "APACHE2.0",
9+
"dependencies": {
10+
"fake-http-request": "*"
11+
},
12+
"optionalDependencies": {
13+
"minimal-request-promise": "*"
14+
}
15+
}

tests/unit/workflows/nodejs_npm/test_actions.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from mock import patch
33

44
from aws_lambda_builders.actions import ActionFailedError
5-
from aws_lambda_builders.workflows.nodejs_npm.actions import NodejsNpmPackAction, NodejsNpmInstallAction
5+
from aws_lambda_builders.workflows.nodejs_npm.actions import \
6+
NodejsNpmPackAction, NodejsNpmInstallAction, NodejsNpmrcCopyAction, NodejsNpmrcCleanUpAction
67
from aws_lambda_builders.workflows.nodejs_npm.npm import NpmExecutionError
78

89

@@ -78,3 +79,77 @@ def test_raises_action_failed_when_npm_fails(self, SubprocessNpmMock):
7879
action.execute()
7980

8081
self.assertEqual(raised.exception.args[0], "NPM Failed: boom!")
82+
83+
84+
class TestNodejsNpmrcCopyAction(TestCase):
85+
86+
@patch("aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils")
87+
def test_copies_npmrc_into_a_project(self, OSUtilMock):
88+
osutils = OSUtilMock.return_value
89+
osutils.joinpath.side_effect = lambda a, b: "{}/{}".format(a, b)
90+
91+
action = NodejsNpmrcCopyAction("artifacts",
92+
"source",
93+
osutils=osutils)
94+
osutils.file_exists.side_effect = [True]
95+
action.execute()
96+
97+
osutils.file_exists.assert_called_with("source/.npmrc")
98+
osutils.copy_file.assert_called_with("source/.npmrc", "artifacts")
99+
100+
@patch("aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils")
101+
def test_skips_copying_npmrc_into_a_project_if_npmrc_doesnt_exist(self, OSUtilMock):
102+
osutils = OSUtilMock.return_value
103+
osutils.joinpath.side_effect = lambda a, b: "{}/{}".format(a, b)
104+
105+
action = NodejsNpmrcCopyAction("artifacts",
106+
"source",
107+
osutils=osutils)
108+
osutils.file_exists.side_effect = [False]
109+
action.execute()
110+
111+
osutils.file_exists.assert_called_with("source/.npmrc")
112+
osutils.copy_file.assert_not_called()
113+
114+
@patch("aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils")
115+
def test_raises_action_failed_when_copying_fails(self, OSUtilMock):
116+
osutils = OSUtilMock.return_value
117+
osutils.joinpath.side_effect = lambda a, b: "{}/{}".format(a, b)
118+
119+
osutils.copy_file.side_effect = OSError()
120+
121+
action = NodejsNpmrcCopyAction("artifacts",
122+
"source",
123+
osutils=osutils)
124+
125+
with self.assertRaises(ActionFailedError):
126+
action.execute()
127+
128+
129+
class TestNodejsNpmrcCleanUpAction(TestCase):
130+
131+
@patch("aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils")
132+
def test_removes_npmrc_if_npmrc_exists(self, OSUtilMock):
133+
osutils = OSUtilMock.return_value
134+
osutils.joinpath.side_effect = lambda a, b: "{}/{}".format(a, b)
135+
136+
action = NodejsNpmrcCleanUpAction(
137+
"artifacts",
138+
osutils=osutils)
139+
osutils.file_exists.side_effect = [True]
140+
action.execute()
141+
142+
osutils.remove_file.assert_called_with("artifacts/.npmrc")
143+
144+
@patch("aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils")
145+
def test_skips_npmrc_removal_if_npmrc_doesnt_exist(self, OSUtilMock):
146+
osutils = OSUtilMock.return_value
147+
osutils.joinpath.side_effect = lambda a, b: "{}/{}".format(a, b)
148+
149+
action = NodejsNpmrcCleanUpAction(
150+
"artifacts",
151+
osutils=osutils)
152+
osutils.file_exists.side_effect = [False]
153+
action.execute()
154+
155+
osutils.remove_file.assert_not_called()

tests/unit/workflows/nodejs_npm/test_workflow.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from aws_lambda_builders.actions import CopySourceAction
44
from aws_lambda_builders.workflows.nodejs_npm.workflow import NodejsNpmWorkflow
5-
from aws_lambda_builders.workflows.nodejs_npm.actions import NodejsNpmPackAction, NodejsNpmInstallAction
5+
from aws_lambda_builders.workflows.nodejs_npm.actions import \
6+
NodejsNpmPackAction, NodejsNpmInstallAction, NodejsNpmrcCopyAction, NodejsNpmrcCleanUpAction
67

78

89
class TestNodejsNpmWorkflow(TestCase):
@@ -16,10 +17,14 @@ def test_workflow_sets_up_npm_actions(self):
1617

1718
workflow = NodejsNpmWorkflow("source", "artifacts", "scratch_dir", "manifest")
1819

19-
self.assertEqual(len(workflow.actions), 3)
20+
self.assertEqual(len(workflow.actions), 5)
2021

2122
self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction)
2223

23-
self.assertIsInstance(workflow.actions[1], CopySourceAction)
24+
self.assertIsInstance(workflow.actions[1], NodejsNpmrcCopyAction)
2425

25-
self.assertIsInstance(workflow.actions[2], NodejsNpmInstallAction)
26+
self.assertIsInstance(workflow.actions[2], CopySourceAction)
27+
28+
self.assertIsInstance(workflow.actions[3], NodejsNpmInstallAction)
29+
30+
self.assertIsInstance(workflow.actions[4], NodejsNpmrcCleanUpAction)

0 commit comments

Comments
 (0)