Repository: lektor/lektor-webpack-support

Issue:
Yarn support
Would you be open to a PR adding support for using [`yarn`](https://yarnpkg.com/) in place of `npm install`? It both runs faster and uses a lockfile (for package version pinning). The implementation I have in mind would [rename and] update `npm_install()` to check for a `yarn` executable and a `.../webpack/yarn.lock` file, using `yarn` in that case or defaulting to current behavior.

Discussion:

Patch:
diff --git a/README.md b/README.md
index 5804c63..581e9bb 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ but not important for functionality, change them to suit your own needs):
 }
 ```
 
-Now we can npm install the rest:
+Now we can `npm install` (or `yarn add`) the rest:
 
 ```
 $ cd </path/to/your/lektor/project>/webpack
diff --git a/lektor_webpack_support.py b/lektor_webpack_support.py
index 11e35f8..cc05e09 100644
--- a/lektor_webpack_support.py
+++ b/lektor_webpack_support.py
@@ -2,7 +2,7 @@ import os
 
 from lektor.pluginsystem import Plugin
 from lektor.reporter import reporter
-from lektor.utils import portable_popen
+from lektor.utils import locate_executable, portable_popen
 
 
 class WebpackSupportPlugin(Plugin):
@@ -23,16 +23,24 @@ class WebpackSupportPlugin(Plugin):
             args.append('--watch')
         return portable_popen(args, cwd=webpack_root)
 
-    def npm_install(self):
-        reporter.report_generic('Running npm install')
+    def install_node_dependencies(self):
         webpack_root = os.path.join(self.env.root_path, 'webpack')
-        portable_popen(['npm', 'install'], cwd=webpack_root).wait()
+
+        # Use yarn over npm if it's availabe and there is a yarn lockfile
+        has_yarn_lockfile = os.path.exists(os.path.join(
+            webpack_root, 'yarn.lock'))
+        pkg_manager = 'npm'
+        if locate_executable('yarn') is not None and has_yarn_lockfile:
+            pkg_manager = 'yarn'
+
+        reporter.report_generic('Running {} install'.format(pkg_manager))
+        portable_popen([pkg_manager, 'install'], cwd=webpack_root).wait()
 
     def on_server_spawn(self, **extra):
         extra_flags = extra.get("extra_flags") or extra.get("build_flags") or {}
         if not self.is_enabled(extra_flags):
             return
-        self.npm_install()
+        self.install_node_dependencies()
         reporter.report_generic('Spawning webpack watcher')
         self.webpack_process = self.run_webpack(watch=True)
 
@@ -48,7 +56,7 @@ class WebpackSupportPlugin(Plugin):
         if not self.is_enabled(extra_flags) \
            or self.webpack_process is not None:
             return
-        self.npm_install()
+        self.install_node_dependencies()
         reporter.report_generic('Starting webpack build')
         self.run_webpack().wait()
         reporter.report_generic('Webpack build finished')


Test patch:
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 96cecd3..764fa0a 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -66,3 +66,25 @@ def test_server_stop(plugin, mocker):
     plugin.webpack_process = mocker.Mock()
     plugin.on_server_stop()
     plugin.webpack_process.kill.assert_called_with()
+
+
+def test_yarn_support(plugin, env, mocker):
+    mock_popen = mocker.patch('lektor_webpack_support.portable_popen')
+
+    mock_path_exists = mocker.patch('os.path.exists')
+    mock_path_exists.return_value = True
+
+    mock_locate_executable = mocker.patch('lektor_webpack_support.locate_executable')
+    mock_locate_executable.return_value = '/usr/local/bin/yarn'
+
+    env_path = py.path.local(env.root_path)
+    plugin.install_node_dependencies()
+
+    assert mock_path_exists.called
+    assert mock_path_exists.call_args[0][0].endswith('yarn.lock')
+    assert mock_locate_executable.called
+    mock_locate_executable.assert_any_call('yarn')
+    mock_popen.assert_any_call(
+        ['yarn', 'install'],
+        cwd=env_path / 'webpack'
+    )