diff --git a/frontend-maven-plugin/src/it/npm-version-from-engines/package.json b/frontend-maven-plugin/src/it/npm-version-from-engines/package.json
new file mode 100644
index 00000000..bd375490
--- /dev/null
+++ b/frontend-maven-plugin/src/it/npm-version-from-engines/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "example",
+ "version": "0.0.1",
+ "engines": {
+ "npm": ">=7 <8"
+ },
+ "dependencies": {
+ "less": "~3.0.2"
+ },
+ "scripts": {
+ "prebuild": "npm install"
+ }
+}
diff --git a/frontend-maven-plugin/src/it/npm-version-from-engines/pom.xml b/frontend-maven-plugin/src/it/npm-version-from-engines/pom.xml
new file mode 100644
index 00000000..8c1a8fd9
--- /dev/null
+++ b/frontend-maven-plugin/src/it/npm-version-from-engines/pom.xml
@@ -0,0 +1,50 @@
+
+
+ 4.0.0
+
+ com.github.eirslett
+ example
+ 0
+ pom
+
+
+
+
+ com.github.eirslett
+ frontend-maven-plugin
+
+ @project.version@
+
+
+ target
+
+
+
+
+
+ install node and npm
+
+ install-node-and-npm
+
+
+ v16.0.0
+ engines
+
+
+
+
+ npm install
+
+ npm
+
+
+
+ install
+
+
+
+
+
+
+
+
diff --git a/frontend-maven-plugin/src/it/npm-version-from-engines/verify.groovy b/frontend-maven-plugin/src/it/npm-version-from-engines/verify.groovy
new file mode 100644
index 00000000..0a9d23ea
--- /dev/null
+++ b/frontend-maven-plugin/src/it/npm-version-from-engines/verify.groovy
@@ -0,0 +1,9 @@
+assert new File(basedir, 'target/node').exists() : "Node was not installed in the custom install directory";
+assert new File(basedir, 'node_modules').exists() : "Node modules were not installed in the base directory";
+assert new File(basedir, 'target/node/npm').exists() : "npm was not copied to the node directory";
+
+import org.codehaus.plexus.util.FileUtils;
+
+String buildLog = FileUtils.fileRead(new File(basedir, 'build.log'));
+
+assert buildLog.contains('BUILD SUCCESS') : 'build was not successful'
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NPMInstaller.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NPMInstaller.java
index 34f15647..58352a0a 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NPMInstaller.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NPMInstaller.java
@@ -6,6 +6,10 @@
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import com.vdurmont.semver4j.Requirement;
+import com.vdurmont.semver4j.Semver;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -28,6 +32,8 @@ public class NPMInstaller {
private final FileDownloader fileDownloader;
+ private Requirement npmVersionRequirement;
+
NPMInstaller(InstallConfig config, ArchiveExtractor archiveExtractor, FileDownloader fileDownloader) {
this.logger = LoggerFactory.getLogger(getClass());
this.config = config;
@@ -78,7 +84,53 @@ public void install() throws InstallationException {
if (this.npmDownloadRoot == null || this.npmDownloadRoot.isEmpty()) {
this.npmDownloadRoot = DEFAULT_NPM_DOWNLOAD_ROOT;
}
+ if ("engines".equals(this.npmVersion)) {
+ try {
+ File packageFile = new File(this.config.getWorkingDirectory(), "package.json");
+ HashMap data = new ObjectMapper().readValue(packageFile, HashMap.class);
+ if (data.containsKey("engines")) {
+ HashMap engines = (HashMap) data.get("engines");
+ if (engines.containsKey("npm")) {
+ this.npmVersionRequirement = Requirement.buildNPM((String) engines.get("npm"));
+ } else {
+ this.logger.info("Could not read npm from engines from package.json");
+ }
+ } else {
+ this.logger.info("Could not read engines from package.json");
+ }
+ } catch (IOException e) {
+ throw new InstallationException("Could not read npm engine version from package.json", e);
+ }
+ }
+
if (!npmProvided() && !npmIsAlreadyInstalled()) {
+ if (this.npmVersionRequirement != null) {
+ // download available node versions
+ try {
+ String downloadUrl = this.npmDownloadRoot
+ + "..";
+
+ File archive = File.createTempFile("npm_versions", ".json");
+
+ downloadFile(downloadUrl, archive, this.userName, this.password);
+
+ HashMap data = new ObjectMapper().readValue(archive, HashMap.class);
+
+ List npmVersions = new LinkedList<>();
+ if (data.containsKey("versions")) {
+ HashMap versions = (HashMap) data.get("versions");
+ npmVersions.addAll(versions.keySet());
+ } else {
+ this.logger.info("Could not read versions from NPM registry");
+ }
+
+ logger.debug("Available NPM versions: {}", npmVersions);
+ this.npmVersion = npmVersions.stream().filter(version -> npmVersionRequirement.isSatisfiedBy(new Semver(version, Semver.SemverType.NPM))).findFirst().orElseThrow(() -> new InstallationException("Could not find matching node version satisfying requirement " + this.npmVersionRequirement));
+ this.logger.info("Found matching NPM version {} satisfying requirement {}.", this.npmVersion, this.npmVersionRequirement);
+ } catch (IOException | DownloadException e) {
+ throw new InstallationException("Could not get available node versions.", e);
+ }
+ }
installNpm();
}
copyNpmScripts();
@@ -93,7 +145,12 @@ private boolean npmIsAlreadyInstalled() {
HashMap data = new ObjectMapper().readValue(npmPackageJson, HashMap.class);
if (data.containsKey(VERSION)) {
final String foundNpmVersion = data.get(VERSION).toString();
- if (foundNpmVersion.equals(this.npmVersion)) {
+ if (npmVersionRequirement != null && npmVersionRequirement.isSatisfiedBy(new Semver(foundNpmVersion, Semver.SemverType.NPM))) {
+ //update version with installed version
+ this.nodeVersion = foundNpmVersion;
+ this.logger.info("NPM {} matches required version range {} installed.", foundNpmVersion, npmVersionRequirement);
+ return true;
+ } else if (foundNpmVersion.equals(this.npmVersion)) {
this.logger.info("NPM {} is already installed.", foundNpmVersion);
return true;
} else {