diff --git a/frontend-maven-plugin/src/it/node-version-from-engines/package.json b/frontend-maven-plugin/src/it/node-version-from-engines/package.json
new file mode 100644
index 00000000..1f129e6f
--- /dev/null
+++ b/frontend-maven-plugin/src/it/node-version-from-engines/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "example",
+ "version": "0.0.1",
+ "engines": {
+ "node": ">=10.3 <15"
+ },
+ "dependencies": {
+ "less": "~3.0.2"
+ },
+ "scripts": {
+ "prebuild": "npm install"
+ }
+}
diff --git a/frontend-maven-plugin/src/it/node-version-from-engines/pom.xml b/frontend-maven-plugin/src/it/node-version-from-engines/pom.xml
new file mode 100644
index 00000000..fc849085
--- /dev/null
+++ b/frontend-maven-plugin/src/it/node-version-from-engines/pom.xml
@@ -0,0 +1,49 @@
+
+
+ 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
+
+
+ engines
+
+
+
+
+ npm install
+
+ npm
+
+
+
+ install
+
+
+
+
+
+
+
+
diff --git a/frontend-maven-plugin/src/it/node-version-from-engines/verify.groovy b/frontend-maven-plugin/src/it/node-version-from-engines/verify.groovy
new file mode 100644
index 00000000..0a9d23ea
--- /dev/null
+++ b/frontend-maven-plugin/src/it/node-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-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-maven-plugin/src/it/yarn-version-from-engines/package.json b/frontend-maven-plugin/src/it/yarn-version-from-engines/package.json
new file mode 100644
index 00000000..e3ec4338
--- /dev/null
+++ b/frontend-maven-plugin/src/it/yarn-version-from-engines/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "example",
+ "version": "0.0.1",
+ "engines": {
+ "yarn": "^1.10"
+ },
+ "dependencies": {
+ "less": "~3.0.2"
+ },
+ "scripts": {
+ "prebuild": "npm install"
+ }
+}
diff --git a/frontend-maven-plugin/src/it/yarn-version-from-engines/pom.xml b/frontend-maven-plugin/src/it/yarn-version-from-engines/pom.xml
new file mode 100644
index 00000000..7e72c4c3
--- /dev/null
+++ b/frontend-maven-plugin/src/it/yarn-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 yarn
+
+ install-node-and-yarn
+
+
+ v16.0.0
+ engines
+
+
+
+
+ yarn install
+
+ yarn
+
+
+
+ install
+
+
+
+
+
+
+
+
diff --git a/frontend-maven-plugin/src/it/yarn-version-from-engines/verify.groovy b/frontend-maven-plugin/src/it/yarn-version-from-engines/verify.groovy
new file mode 100644
index 00000000..1355cd8e
--- /dev/null
+++ b/frontend-maven-plugin/src/it/yarn-version-from-engines/verify.groovy
@@ -0,0 +1,6 @@
+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, 'node_modules/less/package.json').exists() : "Less dependency has not been installed successfully";
+
+String buildLog = new File(basedir, 'build.log').text
+assert buildLog.contains('BUILD SUCCESS') : 'build was not successful'
diff --git a/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndNpmMojo.java b/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndNpmMojo.java
index c80b3c0a..c163ff42 100644
--- a/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndNpmMojo.java
+++ b/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndNpmMojo.java
@@ -78,7 +78,7 @@ public void execute(FrontendPluginFactory factory) throws InstallationException
String npmDownloadRoot = getNpmDownloadRoot();
Server server = MojoUtils.decryptServer(serverId, session, decrypter);
if (null != server) {
- factory.getNodeInstaller(proxyConfig)
+ String installedNodeVersion=factory.getNodeInstaller(proxyConfig)
.setNodeVersion(nodeVersion)
.setNodeDownloadRoot(nodeDownloadRoot)
.setNpmVersion(npmVersion)
@@ -86,20 +86,20 @@ public void execute(FrontendPluginFactory factory) throws InstallationException
.setPassword(server.getPassword())
.install();
factory.getNPMInstaller(proxyConfig)
- .setNodeVersion(nodeVersion)
+ .setNodeVersion(installedNodeVersion)
.setNpmVersion(npmVersion)
.setNpmDownloadRoot(npmDownloadRoot)
.setUserName(server.getUsername())
.setPassword(server.getPassword())
.install();
} else {
- factory.getNodeInstaller(proxyConfig)
+ String installedNodeVersion=factory.getNodeInstaller(proxyConfig)
.setNodeVersion(nodeVersion)
.setNodeDownloadRoot(nodeDownloadRoot)
.setNpmVersion(npmVersion)
.install();
factory.getNPMInstaller(proxyConfig)
- .setNodeVersion(this.nodeVersion)
+ .setNodeVersion(installedNodeVersion)
.setNpmVersion(this.npmVersion)
.setNpmDownloadRoot(npmDownloadRoot)
.install();
diff --git a/frontend-plugin-core/pom.xml b/frontend-plugin-core/pom.xml
index 6cf18238..f3f7a014 100644
--- a/frontend-plugin-core/pom.xml
+++ b/frontend-plugin-core/pom.xml
@@ -11,6 +11,12 @@
jar
+
+ com.vdurmont
+ semver4j
+ 3.1.0
+
+
com.fasterxml.jackson.core
jackson-core
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 9df7a255..7cf5b430 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 {
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeInstaller.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeInstaller.java
index 5b5c6819..5019b3a1 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeInstaller.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeInstaller.java
@@ -7,7 +7,13 @@
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
-
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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 +34,8 @@ public class NodeInstaller {
private final FileDownloader fileDownloader;
+ private Requirement nodeVersionRequirement;
+
NodeInstaller(InstallConfig config, ArchiveExtractor archiveExtractor, FileDownloader fileDownloader) {
this.logger = LoggerFactory.getLogger(getClass());
this.config = config;
@@ -74,13 +82,65 @@ private boolean npmProvided() throws InstallationException {
return false;
}
- public void install() throws InstallationException {
+ public String install() throws InstallationException {
// use static lock object for a synchronized block
synchronized (LOCK) {
if (this.nodeDownloadRoot == null || this.nodeDownloadRoot.isEmpty()) {
this.nodeDownloadRoot = this.config.getPlatform().getNodeDownloadRoot();
}
+
+ if ("engines".equals(this.nodeVersion)) {
+ 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("node")) {
+ this.nodeVersionRequirement = Requirement.buildNPM((String) engines.get("node"));
+ } else {
+ this.logger.info("Could not read node 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 node engine version from package.json", e);
+ }
+ }
+
if (!nodeIsAlreadyInstalled()) {
+ if (this.nodeVersionRequirement != null) {
+ // download available node versions
+ try {
+ String downloadUrl = this.nodeDownloadRoot
+ + "index.json";
+
+ File tmpDirectory = getTempDirectory();
+
+ File archive = File.createTempFile("node_versions", ".json", tmpDirectory);
+
+ downloadFile(downloadUrl, archive, this.userName, this.password);
+
+ HashMap[] data = new ObjectMapper().readValue(archive, HashMap[].class);
+
+ List nodeVersions = new LinkedList<>();
+ for (HashMap d : data) {
+ if (d.containsKey("version")) {
+ nodeVersions.add((String) d.get("version"));
+ }
+ }
+
+ // we want the oldest possible version, that satisfies the requirements
+ Collections.reverse(nodeVersions);
+
+ logger.debug("Available node versions: {}", nodeVersions);
+ this.nodeVersion = nodeVersions.stream().filter(version -> nodeVersionRequirement.isSatisfiedBy(new Semver(version, Semver.SemverType.NPM))).findFirst().orElseThrow(() -> new InstallationException("Could not find matching node version satisfying requirement " + this.nodeVersionRequirement));
+ this.logger.info("Found matching node version {} satisfying requirement {}.", this.nodeVersion, this.nodeVersionRequirement);
+ } catch (IOException | DownloadException e) {
+ throw new InstallationException("Could not get available node versions.", e);
+ }
+ }
+
this.logger.info("Installing node version {}", this.nodeVersion);
if (!this.nodeVersion.startsWith("v")) {
this.logger.warn("Node version does not start with naming convention 'v'.");
@@ -96,6 +156,8 @@ public void install() throws InstallationException {
}
}
}
+
+ return nodeVersion;
}
private boolean nodeIsAlreadyInstalled() {
@@ -104,14 +166,19 @@ private boolean nodeIsAlreadyInstalled() {
File nodeFile = executorConfig.getNodePath();
if (nodeFile.exists()) {
final String version =
- new NodeExecutor(executorConfig, Arrays.asList("--version"), null).executeAndGetResult(logger);
+ new NodeExecutor(executorConfig, Arrays.asList("--version"), null).executeAndGetResult(logger);
- if (version.equals(this.nodeVersion)) {
+ if (nodeVersionRequirement != null && nodeVersionRequirement.isSatisfiedBy(new Semver(version, Semver.SemverType.NPM))) {
+ //update version with installed version
+ this.nodeVersion = version;
+ this.logger.info("Node {} matches required version range {} installed.", version, nodeVersionRequirement);
+ return true;
+ } else if (version.equals(this.nodeVersion)) {
this.logger.info("Node {} is already installed.", version);
return true;
} else {
this.logger.info("Node {} was installed, but we need version {}", version,
- this.nodeVersion);
+ this.nodeVersion);
return false;
}
} else {
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnInstaller.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnInstaller.java
index b7445d99..1ae5a6f9 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnInstaller.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnInstaller.java
@@ -5,7 +5,14 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
-
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.vdurmont.semver4j.Requirement;
+import com.vdurmont.semver4j.Semver;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,6 +40,8 @@ public class YarnInstaller {
private final FileDownloader fileDownloader;
+ private Requirement yarnVersionRequirement;
+
YarnInstaller(InstallConfig config, ArchiveExtractor archiveExtractor, FileDownloader fileDownloader) {
logger = LoggerFactory.getLogger(getClass());
this.config = config;
@@ -71,7 +80,53 @@ public void install() throws InstallationException {
if (yarnDownloadRoot == null || yarnDownloadRoot.isEmpty()) {
yarnDownloadRoot = DEFAULT_YARN_DOWNLOAD_ROOT;
}
+ if ("engines".equals(this.yarnVersion)) {
+ 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("yarn")) {
+ this.yarnVersionRequirement = Requirement.buildNPM((String) engines.get("yarn"));
+ } else {
+ this.logger.info("Could not read yarn 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 yarn engine version from package.json", e);
+ }
+ }
if (!yarnIsAlreadyInstalled()) {
+ if (this.yarnVersionRequirement != null) {
+ // download available node versions
+ try {
+ String downloadUrl = "https://api.github.com/repos/yarnpkg/yarn/releases";
+
+ File archive = File.createTempFile("yarn_versions", ".json");
+
+ downloadFile(downloadUrl, archive, this.userName, this.password);
+
+ HashMap[] data = new ObjectMapper().readValue(archive, HashMap[].class);
+
+ List yarnVersions = new LinkedList<>();
+ for (HashMap d : data) {
+ if (d.containsKey("name")) {
+ yarnVersions.add((String) d.get("name"));
+ }
+ }
+
+ // we want the oldest possible version, that satisfies the requirements
+ Collections.reverse(yarnVersions);
+
+ logger.debug("Available Yarn versions: {}", yarnVersions);
+ this.yarnVersion = yarnVersions.stream().filter(version -> yarnVersionRequirement.isSatisfiedBy(new Semver(version, Semver.SemverType.NPM))).findFirst().orElseThrow(() -> new InstallationException("Could not find matching node version satisfying requirement " + this.yarnVersionRequirement));
+ this.logger.info("Found matching Yarn version {} satisfying requirement {}.", this.yarnVersion, this.yarnVersionRequirement);
+ } catch (IOException | DownloadException e) {
+ throw new InstallationException("Could not get available Yarn versions.", e);
+ }
+ }
if (!yarnVersion.startsWith("v")) {
throw new InstallationException("Yarn version has to start with prefix 'v'.");
}
@@ -88,7 +143,12 @@ private boolean yarnIsAlreadyInstalled() {
final String version =
new YarnExecutor(executorConfig, Arrays.asList("--version"), null).executeAndGetResult(logger).trim();
- if (version.equals(yarnVersion.replaceFirst("^v", ""))) {
+ if (yarnVersionRequirement != null && yarnVersionRequirement.isSatisfiedBy(new Semver(version, Semver.SemverType.NPM))) {
+ //update version with installed version
+ this.yarnVersion = version;
+ this.logger.info("Yarn {} matches required version range {} installed.", version, yarnVersionRequirement);
+ return true;
+ } else if (version.equals(yarnVersion.replaceFirst("^v", ""))) {
logger.info("Yarn {} is already installed.", version);
return true;
} else {