diff --git a/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java index d75a1f16309..cbe6e52a43d 100644 --- a/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java +++ b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java @@ -134,14 +134,6 @@ public class BuildDevBundleMojo extends AbstractMojo @Parameter(property = InitParameters.NODE_VERSION, defaultValue = FrontendTools.DEFAULT_NODE_VERSION) private String nodeVersion; - /** - * Setting defining if the automatically installed node version may be - * updated to the default Vaadin node version. - */ - @Parameter(property = InitParameters.NODE_AUTO_UPDATE, defaultValue = "" - + Constants.DEFAULT_NODE_AUTO_UPDATE) - private boolean nodeAutoUpdate; - @Parameter(defaultValue = "${project}", readonly = true, required = true) MavenProject project; @@ -412,11 +404,6 @@ public URI nodeDownloadRoot() throws URISyntaxException { } } - @Override - public boolean nodeAutoUpdate() { - return nodeAutoUpdate; - } - @Override public String nodeVersion() { return nodeVersion; diff --git a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/GradleVersionSupportTest.kt b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/GradleVersionSupportTest.kt index 96f1d3489b6..7fccd579433 100644 --- a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/GradleVersionSupportTest.kt +++ b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/GradleVersionSupportTest.kt @@ -61,7 +61,7 @@ class GradleVersionSupportTest(private val versionUnderTest: GradleVersion) : Ab implementation("org.slf4j:slf4j-simple:$slf4jVersion") } vaadin { - nodeAutoUpdate = true // test the vaadin{} block by changing some innocent property with limited side-effect + eagerServerLoad = false // test the vaadin{} block by changing some innocent property with limited side-effect } """ ) diff --git a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscMultiModuleTest.kt b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscMultiModuleTest.kt index a52ce4f6e44..8e959bfd0c0 100644 --- a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscMultiModuleTest.kt +++ b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscMultiModuleTest.kt @@ -60,7 +60,7 @@ class MiscMultiModuleTest : AbstractGradleTest() { } vaadin { - nodeAutoUpdate = true // test the vaadin{} block by changing some innocent property with limited side-effect + eagerServerLoad = false // test the vaadin{} block by changing some innocent property with limited side-effect } } """.trimIndent()) @@ -104,7 +104,7 @@ class MiscMultiModuleTest : AbstractGradleTest() { } vaadin { - nodeAutoUpdate = true // test the vaadin{} block by changing some innocent property with limited side-effect + eagerServerLoad = false // test the vaadin{} block by changing some innocent property with limited side-effect } } """.trimIndent()) @@ -161,7 +161,7 @@ class MiscMultiModuleTest : AbstractGradleTest() { } vaadin { - nodeAutoUpdate = true // test the vaadin{} block by changing some innocent property with limited side-effect + eagerServerLoad = false // test the vaadin{} block by changing some innocent property with limited side-effect } """.trimIndent()) @@ -212,7 +212,7 @@ class MiscMultiModuleTest : AbstractGradleTest() { } vaadin { - nodeAutoUpdate = true // test the vaadin{} block by changing some innocent property with limited side-effect + eagerServerLoad = false // test the vaadin{} block by changing some innocent property with limited side-effect applicationIdentifier = 'MY_APP_ID' } """.trimIndent()) @@ -266,7 +266,7 @@ class MiscMultiModuleTest : AbstractGradleTest() { } vaadin { - nodeAutoUpdate = true // test the vaadin{} block by changing some innocent property with limited side-effect + eagerServerLoad = false // test the vaadin{} block by changing some innocent property with limited side-effect applicationIdentifier = 'MY_APP_ID' } """.trimIndent()) diff --git a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscSingleModuleTest.kt b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscSingleModuleTest.kt index d01777bff5b..de9d8764ab5 100644 --- a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscSingleModuleTest.kt +++ b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscSingleModuleTest.kt @@ -194,7 +194,7 @@ class MiscSingleModuleTest : AbstractGradleTest() { } def jettyVersion = "11.0.12" vaadin { - nodeAutoUpdate = true // test the vaadin{} block by changing some innocent property with limited side-effect + eagerServerLoad = false // test the vaadin{} block by changing some innocent property with limited side-effect } dependencies { implementation("com.vaadin:flow:$flowVersion") diff --git a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/VaadinSmokeTest.kt b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/VaadinSmokeTest.kt index 859179b9a2e..f4a291a6779 100644 --- a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/VaadinSmokeTest.kt +++ b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/VaadinSmokeTest.kt @@ -57,7 +57,7 @@ class VaadinSmokeTest : AbstractGradleTest() { implementation("org.slf4j:slf4j-simple:$slf4jVersion") } vaadin { - nodeAutoUpdate = true // test the vaadin{} block by changing some innocent property with limited side-effect + eagerServerLoad = false // test the vaadin{} block by changing some innocent property with limited side-effect } """) } @@ -285,7 +285,7 @@ class VaadinSmokeTest : AbstractGradleTest() { implementation("org.slf4j:slf4j-simple:$slf4jVersion") } vaadin { - nodeAutoUpdate = true // test the vaadin{} block by changing some innocent property with limited side-effect + eagerServerLoad = false // test the vaadin{} block by changing some innocent property with limited side-effect } """) testProject.newFolder("libs") @@ -487,7 +487,7 @@ class VaadinSmokeTest : AbstractGradleTest() { assertContains(result.output, "Configuration cache entry stored") val buildFile = testProject.buildFile.readText() - .replace("nodeAutoUpdate = true", "nodeAutoUpdate = false") + .replace("eagerServerLoad = false", "eagerServerLoad = true") testProject.buildFile.writeText(buildFile) val result2 = testProject.build("--configuration-cache", "vaadinPrepareFrontend", checkTasksSuccessful = false) diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt index 7928ac31c57..7874a642707 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt @@ -199,8 +199,6 @@ internal class GradlePluginAdapter private constructor( override fun nodeDownloadRoot(): URI = URI.create(config.nodeDownloadRoot.get()) - override fun nodeAutoUpdate(): Boolean = config.nodeAutoUpdate.get() - override fun nodeVersion(): String = config.nodeVersion.get() override fun npmFolder(): File = config.npmFolder.get() diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt index c1a292cf225..e0eab9c8074 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt @@ -125,9 +125,6 @@ internal class PrepareFrontendInputProperties( @Input fun getNodeDownloadRoot(): Provider = config.nodeDownloadRoot - @Input - fun getNodeAutoUpdate(): Provider = config.nodeAutoUpdate - @Input fun getProjectBuildDir(): Provider = config.projectBuildDir diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt index 36c6b97e9a8..db4a630de3b 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt @@ -189,11 +189,6 @@ public abstract class VaadinFlowPluginExtension @Inject constructor(private val */ public abstract val nodeDownloadRoot: Property - /** - * Allow automatic update of node installed to alternate location. Default `false` - */ - public abstract val nodeAutoUpdate: Property - /** * Defines the output directory for generated non-served resources, such as * the token file. Defaults to `build/vaadin-generated` folder. @@ -502,9 +497,6 @@ public class PluginEffectiveConfiguration( public val nodeDownloadRoot: Property = extension.nodeDownloadRoot .convention(Platform.guess().nodeDownloadRoot) - public val nodeAutoUpdate: Property = extension.nodeAutoUpdate - .convention(false) - public val resourceOutputDirectory: Property = extension.resourceOutputDirectory .convention( @@ -671,7 +663,6 @@ public class PluginEffectiveConfiguration( "generatedTsFolder=${generatedTsFolder.get()}, " + "nodeVersion=${nodeVersion.get()}, " + "nodeDownloadRoot=${nodeDownloadRoot.get()}, " + - "nodeAutoUpdate=${nodeAutoUpdate.get()}, " + "resourceOutputDirectory=${resourceOutputDirectory.get()}, " + "projectBuildDir=${projectBuildDir.get()}, " + "postinstallPackages=${postinstallPackages.get()}, " + diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java index 8b0d0e8f44e..6be9660d729 100644 --- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java +++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java @@ -133,14 +133,6 @@ public abstract class FlowModeAbstractMojo extends AbstractMojo @Parameter(property = InitParameters.NODE_VERSION, defaultValue = FrontendTools.DEFAULT_NODE_VERSION) private String nodeVersion; - /** - * Setting defining if the automatically installed node version may be - * updated to the default Vaadin node version. - */ - @Parameter(property = InitParameters.NODE_AUTO_UPDATE, defaultValue = "" - + Constants.DEFAULT_NODE_AUTO_UPDATE) - private boolean nodeAutoUpdate; - /** * The folder where `package.json` file is located. Default is project root * dir. @@ -586,11 +578,6 @@ public URI nodeDownloadRoot() throws URISyntaxException { } } - @Override - public boolean nodeAutoUpdate() { - return nodeAutoUpdate; - } - @Override public String nodeVersion() { diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java index 1077d3b128f..5d6687a17bb 100644 --- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java +++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java @@ -173,7 +173,6 @@ protected void executeInternal() .withFrontendGeneratedFolder(generatedTsFolder()) .withNodeVersion(nodeVersion()) .withNodeDownloadRoot(nodeDownloadRoot()) - .setNodeAutoUpdate(nodeAutoUpdate()) .withHomeNodeExecRequired(requireHomeNodeExec()) .setJavaResourceFolder(javaResourceFolder()) .withProductionMode(productionMode) diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java index 00a41ed703b..efd35eb0fd0 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java @@ -146,7 +146,6 @@ public static void prepareFrontend(PluginAdapterBase adapter) FrontendToolsSettings settings = getFrontendToolsSettings(adapter); FrontendTools tools = new FrontendTools(settings); - tools.validateNodeAndNpmVersion(); ClassFinder classFinder = adapter.getClassFinder(); Lookup lookup = adapter.createLookup(classFinder); @@ -166,7 +165,6 @@ public static void prepareFrontend(PluginAdapterBase adapter) getGeneratedFrontendDirectory(adapter)) .withNodeVersion(adapter.nodeVersion()) .withNodeDownloadRoot(nodeDownloadRootURI) - .setNodeAutoUpdate(adapter.nodeAutoUpdate()) .withHomeNodeExecRequired(adapter.requireHomeNodeExec()) .setJavaResourceFolder(adapter.javaResourceFolder()) .withProductionMode(false).withReact(adapter.isReactEnabled()) @@ -210,7 +208,6 @@ private static FrontendToolsSettings getFrontendToolsSettings( () -> FrontendUtils.getVaadinHomeDirectory().getAbsolutePath()); settings.setNodeDownloadRoot(adapter.nodeDownloadRoot()); settings.setNodeVersion(adapter.nodeVersion()); - settings.setAutoUpdate(adapter.nodeAutoUpdate()); settings.setUseGlobalPnpm(adapter.useGlobalPnpm()); settings.setForceAlternativeNode(adapter.requireHomeNodeExec()); settings.setIgnoreVersionChecks( @@ -361,7 +358,6 @@ public static void runNodeUpdater(PluginAdapterBuild adapter, .withHomeNodeExecRequired(adapter.requireHomeNodeExec()) .withNodeVersion(adapter.nodeVersion()) .withNodeDownloadRoot(nodeDownloadRootURI) - .setNodeAutoUpdate(adapter.nodeAutoUpdate()) .setJavaResourceFolder(adapter.javaResourceFolder()) .withPostinstallPackages(adapter.postinstallPackages()) .withCiBuild(adapter.ciBuild()) @@ -434,7 +430,6 @@ public static void runDevBuildNodeUpdater(PluginAdapterBuild adapter) .withHomeNodeExecRequired(adapter.requireHomeNodeExec()) .withNodeVersion(adapter.nodeVersion()) .withNodeDownloadRoot(nodeDownloadRootURI) - .setNodeAutoUpdate(adapter.nodeAutoUpdate()) .setJavaResourceFolder(adapter.javaResourceFolder()) .withPostinstallPackages(adapter.postinstallPackages()) .withBundleBuild(true) @@ -507,7 +502,6 @@ public static void runFrontendBuild(PluginAdapterBase adapter) FrontendToolsSettings settings = getFrontendToolsSettings(adapter); FrontendTools tools = new FrontendTools(settings); - tools.validateNodeAndNpmVersion(); BuildFrontendUtil.runVite(adapter, tools); String tokenContent = ""; File tokenFile = getTokenFile(adapter); @@ -570,12 +564,7 @@ private static void runFrontendBuildTool(PluginAdapterBase adapter, toolName, buildExecutable.getAbsolutePath())); } - String nodePath; - if (adapter.requireHomeNodeExec()) { - nodePath = frontendTools.forceAlternativeNodeExecutable(); - } else { - nodePath = frontendTools.getNodeExecutable(); - } + String nodePath = frontendTools.getNodeExecutable(); List command = new ArrayList<>(); command.add(nodePath); diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/ConvertPolymerCommand.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/ConvertPolymerCommand.java index f0187771098..110c642743b 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/ConvertPolymerCommand.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/ConvertPolymerCommand.java @@ -196,7 +196,6 @@ private FrontendToolsSettings getFrontendToolsSettings() () -> FrontendUtils.getVaadinHomeDirectory().getAbsolutePath()); settings.setNodeDownloadRoot(adapter.nodeDownloadRoot()); settings.setNodeVersion(adapter.nodeVersion()); - settings.setAutoUpdate(adapter.nodeAutoUpdate()); settings.setUseGlobalPnpm(adapter.useGlobalPnpm()); settings.setForceAlternativeNode(adapter.requireHomeNodeExec()); settings.setIgnoreVersionChecks( diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java index 43dd65b9792..583cf71f8a2 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java @@ -193,13 +193,6 @@ default Lookup createLookup(ClassFinder classFinder) { */ URI nodeDownloadRoot() throws URISyntaxException; - /** - * Whether the alternative node may be auto-updated or not. - * - * @return {@code true} to update node if older than default - */ - boolean nodeAutoUpdate(); - /** * The node.js version to be used when node.js is installed automatically by * Vaadin, for example `"v12.18.3"`. Defaults to null which uses the diff --git a/flow-server/src/main/java/com/vaadin/flow/server/Constants.java b/flow-server/src/main/java/com/vaadin/flow/server/Constants.java index c62b73ae21e..921244b7c8b 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/Constants.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/Constants.java @@ -139,11 +139,6 @@ public final class Constants implements Serializable { */ public static final boolean GLOBAL_PNPM_DEFAULT = false; - /** - * The default value for {@link InitParameters#NODE_AUTO_UPDATE}. - */ - public static final boolean DEFAULT_NODE_AUTO_UPDATE = true; - /** * The default value for * {@link InitParameters#REQUIRE_HOME_NODE_EXECUTABLE}. diff --git a/flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java b/flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java index 46aa2dc2af4..4728d4f5d4d 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java @@ -198,13 +198,6 @@ public class InitParameters implements Serializable { */ public static final String REQUIRE_HOME_NODE_EXECUTABLE = "require.home.node"; - /** - * Configuration parameter name for requiring node executable installed in - * home directory. - * - */ - public static final String NODE_AUTO_UPDATE = "node.auto.update"; - /** * Configuration name for the parameter that sets the compiled web * components path. The path should be the same as diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java index c8d11b3a04b..e4b8e2ae4e1 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java @@ -26,7 +26,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Stream; @@ -37,7 +36,6 @@ import com.vaadin.flow.server.InitParameters; import com.vaadin.flow.server.frontend.FrontendUtils.CommandExecutionException; import com.vaadin.flow.server.frontend.FrontendUtils.UnknownVersionException; -import com.vaadin.flow.server.frontend.installer.InstallationException; import com.vaadin.flow.server.frontend.installer.NodeInstaller; import com.vaadin.flow.server.frontend.installer.Platform; import com.vaadin.flow.server.frontend.installer.ProxyConfig; @@ -71,15 +69,6 @@ public class FrontendTools { public static final String DEFAULT_PNPM_VERSION = "8.6.11"; - public static final String INSTALL_NODE_LOCALLY = "%n $ mvn com.github.eirslett:frontend-maven-plugin:1.10.0:install-node-and-npm " - + "-DnodeVersion=\"" + DEFAULT_NODE_VERSION + "\" "; - - public static final String NPM_BIN_PATH = FrontendUtils.isWindows() - ? "node/node_modules/npm/bin/" - : "node/lib/node_modules/npm/bin/"; - - private static final String NPM_BIN_LINUX_LEGACY_PATH = "node/node_modules/npm/bin/"; - private static final String MSG_PREFIX = "%n%n======================================================================================================"; private static final String MSG_SUFFIX = "%n======================================================================================================%n"; @@ -100,11 +89,7 @@ public class FrontendTools { private static final String BAD_VERSION = MSG_PREFIX + "%nYour installed '%s' version (%s) is known to have problems." // - + "%nPlease update to a new one either:" - + "%n - by following the https://nodejs.org/en/download/ guide to install it globally" - + "%s" - + "%n - or by running the frontend-maven-plugin goal to install it in this project:" - + INSTALL_NODE_LOCALLY + "%n" // + + "%nPlease update it%s." + "%n" // + FrontendUtils.DISABLE_CHECK // + MSG_SUFFIX; @@ -116,12 +101,25 @@ public class FrontendTools { private static final int SUPPORTED_NODE_MAJOR_VERSION = 24; private static final int SUPPORTED_NODE_MINOR_VERSION = 0; + /** + * Maximum supported Node.js major version. Versions with a higher major + * version are not tested and may not be compatible. + */ + public static final int MAX_SUPPORTED_NODE_MAJOR_VERSION = 24; private static final int SUPPORTED_NPM_MAJOR_VERSION = 11; private static final int SUPPORTED_NPM_MINOR_VERSION = 3; - static final FrontendVersion SUPPORTED_NODE_VERSION = new FrontendVersion( + public static final FrontendVersion SUPPORTED_NODE_VERSION = new FrontendVersion( SUPPORTED_NODE_MAJOR_VERSION, SUPPORTED_NODE_MINOR_VERSION); + /** + * Minimum Node.js version for auto-installed versions in ~/.vaadin. Global + * installations are accepted if they meet SUPPORTED_NODE_VERSION, but + * auto-installed versions must meet this higher threshold. + */ + public static final FrontendVersion MINIMUM_AUTO_INSTALLED_NODE = new FrontendVersion( + 24, 10, 0); + private static final FrontendVersion SUPPORTED_NPM_VERSION = new FrontendVersion( SUPPORTED_NPM_MAJOR_VERSION, SUPPORTED_NPM_MINOR_VERSION); @@ -168,13 +166,18 @@ String getScript() { private final FrontendToolsLocator frontendToolsLocator = new FrontendToolsLocator(); + // The active node installation - shared across all FrontendTools instances + private static volatile NodeResolver.ActiveNodeInstallation activeNodeInstallation; + + // Lock object for synchronizing node resolution + private static final Object RESOLUTION_LOCK = new Object(); + private final String nodeVersion; private final URI nodeDownloadRoot; private final boolean ignoreVersionChecks; - private boolean forceAlternativeNode; + private final boolean forceAlternativeNode; private final boolean useGlobalPnpm; - private final boolean autoUpdate; /** * Creates an instance of the class using the {@code baseDir} as a base @@ -199,7 +202,6 @@ public FrontendTools(FrontendToolsSettings settings) { this.ignoreVersionChecks = settings.isIgnoreVersionChecks(); this.forceAlternativeNode = settings.isForceAlternativeNode(); this.useGlobalPnpm = settings.isUseGlobalPnpm(); - this.autoUpdate = settings.isAutoUpdate(); } /** @@ -245,10 +247,6 @@ public FrontendTools(ApplicationConfiguration applicationConfiguration, * @param useGlobalPnpm * use globally installed pnpm instead of the default one (see * {@link #DEFAULT_PNPM_VERSION}) - * @param autoUpdate - * update node in {@link #alternativeDirGetter} if version older - * than the current default - * {@value FrontendTools#DEFAULT_NODE_VERSION} * @deprecated use * {@link FrontendTools#FrontendTools(FrontendToolsSettings)} * instead, as it simplifies configuring the frontend tools and @@ -257,18 +255,17 @@ public FrontendTools(ApplicationConfiguration applicationConfiguration, @Deprecated public FrontendTools(String baseDir, Supplier alternativeDirGetter, String nodeVersion, URI nodeDownloadRoot, - boolean forceAlternativeNode, boolean useGlobalPnpm, - boolean autoUpdate) { + boolean forceAlternativeNode, boolean useGlobalPnpm) { this(baseDir, alternativeDirGetter, nodeVersion, nodeDownloadRoot, "true".equalsIgnoreCase(System.getProperty( FrontendUtils.PARAM_IGNORE_VERSION_CHECKS)), - forceAlternativeNode, useGlobalPnpm, autoUpdate); + forceAlternativeNode, useGlobalPnpm); } FrontendTools(String baseDir, Supplier alternativeDirGetter, String nodeVersion, URI nodeDownloadRoot, boolean ignoreVersionChecks, boolean forceAlternativeNode, - boolean useGlobalPnpm, boolean autoUpdate) { + boolean useGlobalPnpm) { this.baseDir = Objects.requireNonNull(baseDir); this.alternativeDirGetter = alternativeDirGetter; this.nodeVersion = Objects.requireNonNull(nodeVersion); @@ -276,7 +273,6 @@ public FrontendTools(String baseDir, Supplier alternativeDirGetter, this.ignoreVersionChecks = ignoreVersionChecks; this.forceAlternativeNode = forceAlternativeNode; this.useGlobalPnpm = useGlobalPnpm; - this.autoUpdate = autoUpdate; } private static FrontendToolsSettings createSettings( @@ -284,8 +280,6 @@ private static FrontendToolsSettings createSettings( File projectRoot) { boolean useHomeNodeExec = applicationConfiguration.getBooleanProperty( InitParameters.REQUIRE_HOME_NODE_EXECUTABLE, false); - boolean nodeAutoUpdate = applicationConfiguration - .getBooleanProperty(InitParameters.NODE_AUTO_UPDATE, false); boolean useGlobalPnpm = applicationConfiguration.getBooleanProperty( InitParameters.SERVLET_PARAMETER_GLOBAL_PNPM, false); final String nodeVersion = applicationConfiguration.getStringProperty( @@ -298,7 +292,6 @@ private static FrontendToolsSettings createSettings( projectRoot.getAbsolutePath(), () -> FrontendUtils.getVaadinHomeDirectory().getAbsolutePath()); settings.setForceAlternativeNode(useHomeNodeExec); - settings.setAutoUpdate(nodeAutoUpdate); settings.setUseGlobalPnpm(useGlobalPnpm); settings.setNodeVersion(nodeVersion); settings.setNodeDownloadRoot(URI.create(nodeDownloadRoot)); @@ -312,149 +305,39 @@ private static FrontendToolsSettings createSettings( * @return the full path to the executable */ public String getNodeExecutable() { - Pair nodeCommands = getNodeCommands(); - File file = getExecutable(baseDir, nodeCommands.getSecond()); - if (file == null && !forceAlternativeNode) { - file = frontendToolsLocator.tryLocateTool(nodeCommands.getFirst()) - .orElse(null); - } - file = rejectUnsupportedNodeVersion(file); - if (file == null) { - file = updateAlternateIfNeeded(getExecutable(getAlternativeDir(), - nodeCommands.getSecond())); - } - if (file == null && alternativeDirGetter != null) { - getLogger().info("Couldn't find {}. Installing Node and npm to {}.", - nodeCommands.getFirst(), getAlternativeDir()); - file = new File(installNode(nodeVersion, nodeDownloadRoot)); - } - if (file == null) { - // This should never happen, because node is automatically installed - // if not detected globally or at project level - throw new IllegalStateException("Node not found"); - } - return file.getAbsolutePath(); + return ensureNodeResolved().nodeExecutable(); } /** - * Update installed node version if installed version is not supported. - *

- * Also update is {@code auto.update} flag set and installed version is - * older than the current default version. + * Ensures that node has been resolved and cached. Uses double-checked + * locking to ensure thread-safe lazy initialization. * - * @param file - * node executable - * @return node executable after possible installation of new version + * @return the active node installation information */ - private File updateAlternateIfNeeded(File file) { - if (file == null) { - return null; + private NodeResolver.ActiveNodeInstallation ensureNodeResolved() { + NodeResolver.ActiveNodeInstallation active = activeNodeInstallation; + if (active != null) { + return active; } - // If auto-update flag set or installed node older than minimum - // supported - try { - List versionCommand = new ArrayList<>(); - versionCommand.add(file.getAbsolutePath()); - versionCommand.add("--version"); // NOSONAR - final FrontendVersion installedNodeVersion = FrontendUtils - .getVersion("node", versionCommand); - - boolean installDefault = false; - final FrontendVersion defaultVersion = new FrontendVersion( - nodeVersion); - if (installedNodeVersion.isOlderThan(SUPPORTED_NODE_VERSION)) { - getLogger().info("Updating unsupported node version {} to {}", - installedNodeVersion.getFullVersion(), - defaultVersion.getFullVersion()); - installDefault = true; - } else if (autoUpdate - && installedNodeVersion.isOlderThan(defaultVersion)) { - getLogger().info( - "Updating current installed node version from {} to {}", - installedNodeVersion.getFullVersion(), - defaultVersion.getFullVersion()); - installDefault = true; - } - if (installDefault) { - file = new File(installNode(nodeVersion, nodeDownloadRoot)); - } - } catch (UnknownVersionException e) { - getLogger().error("Failed to get version for installed node.", e); - } - return file; - } - /** - * Ensures that given node executable is supported by Vaadin. - * - * Returns the input executable if version is supported, otherwise - * {@literal null}. - * - * @param nodeExecutable - * node executable to be checked - * @return input node executable if supported, otherwise {@literal null}. - */ - private File rejectUnsupportedNodeVersion(File nodeExecutable) { - if (nodeExecutable == null) { - return null; - } - try { - List versionCommand = new ArrayList<>(); - versionCommand.add(nodeExecutable.getAbsolutePath()); - versionCommand.add("--version"); // NOSONAR - final FrontendVersion installedNodeVersion = FrontendUtils - .getVersion("node", versionCommand); - - if (installedNodeVersion.isOlderThan(SUPPORTED_NODE_VERSION)) { - getLogger().info( - "{} Node.js version {} is older than the required minimum version {}. Using Node.js from {}.", - nodeExecutable.getPath().startsWith(baseDir) - ? "The project-specific" - : "The globally installed", - installedNodeVersion.getFullVersion(), - SUPPORTED_NODE_VERSION.getFullVersion(), - alternativeDirGetter.get()); - // Global node is not supported use alternative for everything - forceAlternativeNode = true; - return null; + synchronized (RESOLUTION_LOCK) { + // Double-check after acquiring lock + active = activeNodeInstallation; + if (active != null) { + return active; } - } catch (UnknownVersionException e) { - getLogger().error("Failed to get version for installed node.", e); - } - return nodeExecutable; - } - /** - * Locate node executable from the alternative directory given. - * - *

- * The difference between {@link #getNodeExecutable()} and this method in a - * search algorithm: {@link #getNodeExecutable()} first searches executable - * in the base/alternative directory and fallbacks to the globally installed - * if it's not found there. The {@link #forceAlternativeNodeExecutable()} - * doesn't search for globally installed executable. It tries to find it in - * the installation directory and if it's not found it downloads and - * installs it there. - * - * @see #getNodeExecutable() - * - * @return the full path to the executable - */ - public String forceAlternativeNodeExecutable() { - Pair nodeCommands = getNodeCommands(); - String dir = getAlternativeDir(); - File file = new File(dir, nodeCommands.getSecond()); - if (file.exists()) { - if (!frontendToolsLocator.verifyTool(file)) { + // Perform resolution + if (alternativeDirGetter == null) { throw new IllegalStateException( - String.format(LOCAL_NODE_NOT_FOUND, dir, dir, - file.getAbsolutePath())); + "Node not found and no alternative directory configured for installation"); } - return updateAlternateIfNeeded(file).getAbsolutePath(); - } else { - getLogger().info("Node not found in {}. Installing node {}.", dir, - nodeVersion); - return installNode(nodeVersion, nodeDownloadRoot); + + NodeResolver resolver = new NodeResolver(getAlternativeDir(), + nodeVersion, nodeDownloadRoot, forceAlternativeNode, + getProxies()); + activeNodeInstallation = resolver.resolve(); + return activeNodeInstallation; } } @@ -505,36 +388,41 @@ public void validateNodeAndNpmVersion() { if (ignoreVersionChecks) { return; } - Pair foundNodeVersionAndExe = null; + // Node version is already validated by NodeResolver, which ensures + // we have a suitable version (either from global PATH or auto-installed + // to ~/.vaadin). Just log which node we're using. try { - foundNodeVersionAndExe = getNodeVersionAndExecutable(); - FrontendVersion foundNodeVersion = foundNodeVersionAndExe - .getFirst(); + Pair foundNodeVersionAndExe = getNodeVersionAndExecutable(); getLogger().debug("Using node {} located at {}", - foundNodeVersion.getFullVersion(), + foundNodeVersionAndExe.getFirst().getFullVersion(), foundNodeVersionAndExe.getSecond()); - FrontendUtils.validateToolVersion("node", foundNodeVersion, - SUPPORTED_NODE_VERSION); } catch (UnknownVersionException e) { - getLogger().warn("Error checking if node is new enough", e); - } catch (IllegalStateException ise) { - if (foundNodeVersionAndExe != null) { - getLogger().info("Validated node from '{}'", - foundNodeVersionAndExe.getSecond()); - } - throw ise; + getLogger().warn("Error checking node version", e); } + // Validate npm version (npm comes bundled with node) try { FrontendVersion foundNpmVersion = getNpmVersion(); getLogger().debug("Using npm {} located at {}", foundNpmVersion.getFullVersion(), getNpmExecutable(false).get(0)); - FrontendUtils.validateToolVersion("npm", foundNpmVersion, - SUPPORTED_NPM_VERSION); + + // If npm is too old, this is an internal configuration error - the + // node version we accept/install should always come with a suitable + // npm + if (foundNpmVersion.isOlderThan(SUPPORTED_NPM_VERSION)) { + throw new IllegalStateException(String.format( + "Internal error: npm version %s is older than required %s. " + + "This should not happen as Node %s should bundle a compatible npm version. " + + "Please report this issue.", + foundNpmVersion.getFullVersion(), + SUPPORTED_NPM_VERSION.getFullVersion(), + DEFAULT_NODE_VERSION)); + } + checkForFaultyNpmVersion(foundNpmVersion); } catch (UnknownVersionException e) { - getLogger().warn("Error checking if npm is new enough", e); + getLogger().warn("Error checking npm version", e); } } @@ -560,35 +448,6 @@ private Pair getNodeVersionAndExecutable() executable); } - /** - * Install node and npm. - * - * @param nodeVersion - * node version to install - * @param downloadRoot - * optional download root for downloading node. May be a - * filesystem file or a URL see - * {@link NodeInstaller#setNodeDownloadRoot(URI)}. - * @return node installation path - */ - protected String installNode(String nodeVersion, URI downloadRoot) { - NodeInstaller nodeInstaller = new NodeInstaller( - new File(getAlternativeDir()), getProxies()) - .setNodeVersion(nodeVersion); - if (downloadRoot != null) { - nodeInstaller.setNodeDownloadRoot(downloadRoot); - } - - try { - nodeInstaller.install(); - } catch (InstallationException e) { - throw new IllegalStateException("Failed to install Node", e); - } - - return new File(nodeInstaller.getInstallDirectory(), - getNodeCommands().getFirst()).toString(); - } - /** * Read list of configured proxies in order from system properties, .npmrc * file in the project root folder, .npmrc file in user root folder and @@ -755,22 +614,6 @@ public Map getWebpackNodeEnvironment() { return environment; } - private File getExecutable(String dir, String location) { - File file = new File(dir, location); - if (frontendToolsLocator.verifyTool(file)) { - return file; - } - return null; - } - - private Pair getNodeCommands() { - if (FrontendUtils.isWindows()) { - return new Pair<>("node.exe", "node/node.exe"); - } else { - return new Pair<>("node", "node/node"); - } - } - private Logger getLogger() { return LoggerFactory.getLogger(FrontendTools.class); } @@ -795,84 +638,35 @@ private List getNpmExecutable(boolean removePnpmLock) { private List getNpmCliToolExecutable(BuildTool cliTool, String... flags) { - // First look for *-cli.js script in project/node_modules - List returnCommand = getNpmScriptCommand(baseDir, - cliTool.getScript()); - boolean alternativeDirChecked = false; - if (returnCommand.isEmpty() && forceAlternativeNode) { - // First look for *-cli.js script in ~/.vaadin/node/node_modules - // only if alternative node takes precedence over all other location - returnCommand = getNpmScriptCommand(getAlternativeDir(), - cliTool.getScript()); - alternativeDirChecked = true; - } - if (returnCommand.isEmpty()) { - // Otherwise look for regular `npm`/`npx` global search path - Optional command = frontendToolsLocator - .tryLocateTool(cliTool.getCommand()) - .map(File::getAbsolutePath); - if (command.isPresent()) { - returnCommand = Collections.singletonList(command.get()); - if (!alternativeDirChecked && cliTool.equals(BuildTool.NPM)) { - try { - List npmVersionCommand = new ArrayList<>( - returnCommand); - npmVersionCommand.add("--version"); // NOSONAR - final FrontendVersion npmVersion = FrontendUtils - .getVersion("npm", npmVersionCommand); - if (npmVersion.isOlderThan(SUPPORTED_NPM_VERSION)) { - // Global npm is older than SUPPORTED_NPM_VERSION. - // Using npm from ~/.vaadin - returnCommand = new ArrayList<>(); - // Force installation if not installed - forceAlternativeNodeExecutable(); - } - } catch (UnknownVersionException uve) { - getLogger().error("Could not determine npm version", - uve); - // Use from alternate directory if global - // version check failed - returnCommand = new ArrayList<>(); - // Force installation if not installed - // as the global version check failed - forceAlternativeNodeExecutable(); - } + List returnCommand = new ArrayList<>(); + + // For npm/npx, we always use the resolved node installation + if (cliTool.equals(BuildTool.NPM) || cliTool.equals(BuildTool.NPX)) { + NodeResolver.ActiveNodeInstallation active = ensureNodeResolved(); + returnCommand.add(active.nodeExecutable()); + + if (cliTool.equals(BuildTool.NPM)) { + returnCommand.add(active.npmCliScript()); + } else { + // NPX is in the same directory as npm, just different filename + File npmCliFile = new File(active.npmCliScript()); + File npxCliFile = new File(npmCliFile.getParentFile(), + "npx-cli.js"); + if (!npxCliFile.exists()) { + throw new IllegalStateException( + "npx-cli.js not found at expected location: " + + npxCliFile.getAbsolutePath()); } + returnCommand.add(npxCliFile.getAbsolutePath()); } } - if (!alternativeDirChecked && returnCommand.isEmpty()) { - // Use alternative if global is not found and alternative location - // is not yet checked - returnCommand = getNpmScriptCommand(getAlternativeDir(), - cliTool.getScript()); - // force alternative to not check global again for these tools - forceAlternativeNode = true; - } if (flags.length > 0) { - returnCommand = new ArrayList<>(returnCommand); Collections.addAll(returnCommand, flags); } return returnCommand; } - private List getNpmScriptCommand(String dir, String scriptName) { - // If `node` is not found in PATH, `node/node_modules/npm/bin/npm` will - // not work because it's a shell or windows script that looks for node - // and will fail. Thus we look for the `npm-cli` node script instead - File file = new File(dir, NPM_BIN_PATH + scriptName); - if (!FrontendUtils.isWindows() && !file.canRead()) { - file = new File(dir, NPM_BIN_LINUX_LEGACY_PATH + scriptName); - } - List returnCommand = new ArrayList<>(); - if (file.canRead()) { - // We return a two element list with node binary and npm-cli script - returnCommand.add(getNodeBinary()); - returnCommand.add(file.getAbsolutePath()); - } - return returnCommand; - } - List getSuitablePnpm() { List pnpmCommand; if (useGlobalPnpm) { @@ -989,11 +783,7 @@ private String getAlternativeDir() { * @return the path to the node binary */ public String getNodeBinary() { - if (forceAlternativeNode) { - return forceAlternativeNodeExecutable(); - } else { - return getNodeExecutable(); - } + return getNodeExecutable(); } private String removeLineBreaks(String str) { diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendToolsSettings.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendToolsSettings.java index c1a8293375f..bdd586f10e9 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendToolsSettings.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendToolsSettings.java @@ -45,7 +45,6 @@ public class FrontendToolsSettings implements Serializable { private boolean ignoreVersionChecks; private boolean forceAlternativeNode = Constants.DEFAULT_REQUIRE_HOME_NODE_EXECUTABLE; private boolean useGlobalPnpm = Constants.GLOBAL_PNPM_DEFAULT; - private boolean autoUpdate = Constants.DEFAULT_NODE_AUTO_UPDATE; /** * Create a tools configuration object. @@ -163,19 +162,6 @@ public void setUseGlobalPnpm(boolean useGlobalPnpm) { this.useGlobalPnpm = useGlobalPnpm; } - /** - * When set to true the alternative version is updated to the latest default - * node version as defined for the framework. - * - * @param autoUpdate - * update node in {@link #alternativeDirGetter} if version older - * than the current default - * {@value FrontendTools#DEFAULT_NODE_VERSION} - */ - public void setAutoUpdate(boolean autoUpdate) { - this.autoUpdate = autoUpdate; - } - /** * Get the defined base dir. * @@ -238,13 +224,4 @@ public boolean isForceAlternativeNode() { public boolean isUseGlobalPnpm() { return useGlobalPnpm; } - - /** - * Check if automatic updates are enabled. - * - * @return automatic update - */ - public boolean isAutoUpdate() { - return autoUpdate; - } } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java index 65957f26c30..647207901b7 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java @@ -68,7 +68,6 @@ import static com.vaadin.flow.server.Constants.COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT; import static com.vaadin.flow.server.Constants.RESOURCES_FRONTEND_DEFAULT; import static com.vaadin.flow.server.Constants.VAADIN_WEBAPP_RESOURCES; -import static com.vaadin.flow.server.frontend.FrontendTools.INSTALL_NODE_LOCALLY; import static java.lang.String.format; /** @@ -347,15 +346,6 @@ public class FrontendUtils { public static final String DISABLE_CHECK = "%nYou can disable the version check using -D%s=true"; - private static final String TOO_OLD = "%n%n======================================================================================================" - + "%nYour installed '%s' version (%s) is too old. Supported versions are %d.%d+" // - + "%nPlease install a new one either:" - + "%n - by following the https://nodejs.org/en/download/ guide to install it globally" - + "%n - or by running the frontend-maven-plugin goal to install it in this project:" - + INSTALL_NODE_LOCALLY + "%n" // - + DISABLE_CHECK // - + "%n======================================================================================================%n"; - // Proxy config properties keys (for both system properties and environment // variables) can be either fully upper case or fully lower case static final String SYSTEM_NOPROXY_PROPERTY_KEY = "NOPROXY"; @@ -767,12 +757,6 @@ public static File getFrontendGeneratedFolder(File frontendDirectory) { return new File(frontendDirectory, GENERATED); } - private static String buildTooOldString(String tool, String version, - int supportedMajor, int supportedMinor) { - return String.format(TOO_OLD, tool, version, supportedMajor, - supportedMinor, PARAM_IGNORE_VERSION_CHECKS); - } - /** * Get directory where project's frontend files are located. * @@ -811,17 +795,6 @@ public static String getUnixPath(Path source) { return source.toString().replaceAll("\\\\", "/"); } - static void validateToolVersion(String tool, FrontendVersion toolVersion, - FrontendVersion supported) { - if (toolVersion.isEqualOrNewer(supported)) { - return; - } - - throw new IllegalStateException(buildTooOldString(tool, - toolVersion.getFullVersion(), supported.getMajorVersion(), - supported.getMinorVersion())); - } - /** * Thrown when detecting the version of a tool fails. */ diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeResolver.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeResolver.java new file mode 100644 index 00000000000..90e881ba3ef --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeResolver.java @@ -0,0 +1,475 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.server.frontend; + +import java.io.File; +import java.io.Serializable; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.server.frontend.FrontendUtils.UnknownVersionException; +import com.vaadin.flow.server.frontend.installer.InstallationException; +import com.vaadin.flow.server.frontend.installer.NodeInstaller; +import com.vaadin.flow.server.frontend.installer.ProxyConfig; + +/** + * Handles the one-time resolution of which Node.js installation to use. + * Performs the following steps in order: + *

    + *
  1. If forceAlternativeNode is true, skip to step 3
  2. + *
  3. Try to find and use node from global PATH if it meets version + * requirements and has npm available
  4. + *
  5. If no suitable global node found, use NodeInstaller to resolve or install + * node in alternative directory (~/.vaadin)
  6. + *
+ *

+ * Once resolved, the result is cached in an {@link ActiveNodeInstallation} + * record. + *

+ * This class is not serializable as it is only used for resolution, not + * storage. The result ({@link ActiveNodeInstallation}) is serializable and can + * be cached. + *

+ * For internal use only. May be renamed or removed in a future release. + * + * @author Vaadin Ltd + */ +class NodeResolver implements java.io.Serializable { + + /** + * Information about the active node/npm installation that will be used. All + * fields are required and non-null. + * + * @param nodeExecutable + * path to node binary + * @param nodeVersion + * node version string (e.g., "24.10.0") + * @param npmCliScript + * path to npm-cli.js script + * @param npmVersion + * npm version string (e.g., "11.3.0") + */ + record ActiveNodeInstallation(String nodeExecutable, String nodeVersion, + String npmCliScript, String npmVersion) implements Serializable { + ActiveNodeInstallation { + Objects.requireNonNull(nodeExecutable); + Objects.requireNonNull(nodeVersion); + Objects.requireNonNull(npmCliScript); + Objects.requireNonNull(npmVersion); + } + } + + private final FrontendToolsLocator frontendToolsLocator = new FrontendToolsLocator(); + + private final String alternativeDir; + private final String nodeVersion; + private final URI nodeDownloadRoot; + private final boolean forceAlternativeNode; + private final List proxies; + + /** + * Creates a resolver with the given configuration. + * + * @param alternativeDir + * directory where node should be installed if not found globally + * @param nodeVersion + * the node version to use/install (e.g., "v24.10.0") + * @param nodeDownloadRoot + * URI to download node from + * @param forceAlternativeNode + * if true, skip global node lookup and go straight to + * alternative directory + * @param proxies + * list of proxy configurations + */ + NodeResolver(String alternativeDir, String nodeVersion, + URI nodeDownloadRoot, boolean forceAlternativeNode, + List proxies) { + this.alternativeDir = Objects.requireNonNull(alternativeDir); + this.nodeVersion = Objects.requireNonNull(nodeVersion); + this.nodeDownloadRoot = Objects.requireNonNull(nodeDownloadRoot); + this.forceAlternativeNode = forceAlternativeNode; + this.proxies = Objects.requireNonNull(proxies); + } + + /** + * Resolves which node installation to use. This method should be called + * once and the result cached. + * + * @return the active node installation information + * @throws IllegalStateException + * if node cannot be found or installed + */ + ActiveNodeInstallation resolve() { + // If forceAlternativeNode is set, skip global lookup + if (!forceAlternativeNode) { + ActiveNodeInstallation globalInstallation = tryUseGlobalNode(); + if (globalInstallation != null) { + return globalInstallation; + } + } + + // Either forceAlternativeNode is true, or global node was unsuitable + return resolveOrInstallAlternativeNode(); + } + + /** + * Tries to use a globally installed node that meets version requirements. + * + * @return the active node installation, or null if global node not found or + * unsuitable + */ + private ActiveNodeInstallation tryUseGlobalNode() { + String nodeCommand = FrontendUtils.isWindows() ? "node.exe" : "node"; + File nodeExecutable = frontendToolsLocator.tryLocateTool(nodeCommand) + .orElse(null); + + if (nodeExecutable == null) { + return null; + } + + // Check if version is acceptable + try { + List versionCommand = new ArrayList<>(); + versionCommand.add(nodeExecutable.getAbsolutePath()); + versionCommand.add("--version"); + FrontendVersion installedNodeVersion = FrontendUtils + .getVersion("node", versionCommand); + + if (installedNodeVersion + .isOlderThan(FrontendTools.SUPPORTED_NODE_VERSION)) { + getLogger().info( + "The globally installed Node.js version {} is older than the required minimum version {}. Using Node.js from {}.", + installedNodeVersion.getFullVersion(), + FrontendTools.SUPPORTED_NODE_VERSION.getFullVersion(), + alternativeDir); + return null; + } + + // Check that major version is within supported range + if (installedNodeVersion + .getMajorVersion() > FrontendTools.MAX_SUPPORTED_NODE_MAJOR_VERSION) { + getLogger().info( + "The globally installed Node.js version {}.x is newer than the maximum supported version {}.x and may not be compatible. Using Node.js from {}.", + installedNodeVersion.getMajorVersion(), + FrontendTools.MAX_SUPPORTED_NODE_MAJOR_VERSION, + alternativeDir); + return null; + } + + // Found suitable global node - now get npm information + String npmCliScript = getGlobalNpmCliScript(nodeExecutable); + if (npmCliScript == null) { + getLogger().debug( + "npm-cli.js not found in global Node.js installation, will use alternative directory"); + return null; + } + + String npmVersion; + try { + npmVersion = FrontendUtils + .getVersion("npm", + List.of(nodeExecutable.getAbsolutePath(), + npmCliScript, "--version")) + .getFullVersion(); + } catch (UnknownVersionException e) { + getLogger().debug( + "Could not determine npm version from global installation", + e); + npmVersion = "unknown"; + } + + getLogger().info("Using globally installed Node.js version {}", + installedNodeVersion.getFullVersion()); + return new ActiveNodeInstallation(nodeExecutable.getAbsolutePath(), + installedNodeVersion.getFullVersion(), npmCliScript, + npmVersion); + } catch (UnknownVersionException e) { + getLogger().error("Failed to get version for installed node.", e); + return null; + } + } + + /** + * Tries to find npm-cli.js in a global Node.js installation. + * + * @param nodeExecutable + * the global node executable + * @return path to npm-cli.js, or null if not found + */ + private String getGlobalNpmCliScript(File nodeExecutable) { + // Global npm is typically installed alongside node + File nodeDir = nodeExecutable.getParentFile(); + boolean isWindows = FrontendUtils.isWindows(); + + // Try common locations relative to node executable + String[] possiblePaths = isWindows + ? new String[] { "..\\node_modules\\npm\\bin\\npm-cli.js" } + : new String[] { "../lib/node_modules/npm/bin/npm-cli.js" }; + + for (String path : possiblePaths) { + File npmCliScript = new File(nodeDir, path); + if (npmCliScript.exists()) { + return npmCliScript.getAbsolutePath(); + } + } + + return null; + } + + /** + * Resolves an existing compatible node installation in the alternative + * directory, or installs a new one. + * + * @return the active node installation information + * @throws IllegalStateException + * if installation fails + */ + private ActiveNodeInstallation resolveOrInstallAlternativeNode() { + File alternativeDirFile = new File(alternativeDir); + NodeInstaller nodeInstaller = new NodeInstaller(alternativeDirFile, + proxies); + if (nodeDownloadRoot != null) { + nodeInstaller.setNodeDownloadRoot(nodeDownloadRoot); + } + + // First, check if the exact requested version is already installed + String versionToUse = nodeVersion; + File nodeExecutable = getNodeExecutableForVersion(alternativeDirFile, + versionToUse); + + if (nodeExecutable.exists()) { + try { + String installedVersion = FrontendUtils + .getVersion("node", List.of( + nodeExecutable.getAbsolutePath(), "--version")) + .getFullVersion(); + + // Normalize versions for comparison + String normalizedInstalled = installedVersion.startsWith("v") + ? installedVersion.substring(1) + : installedVersion; + String normalizedRequested = nodeVersion.startsWith("v") + ? nodeVersion.substring(1) + : nodeVersion; + + if (normalizedInstalled.equals(normalizedRequested)) { + getLogger().info("Node {} is already installed in {}", + nodeVersion, alternativeDir); + return createActiveInstallation(nodeExecutable, + versionToUse, alternativeDirFile); + } + } catch (UnknownVersionException e) { + getLogger().debug( + "Could not verify version of existing node installation", + e); + } + } + + // Check if any other compatible version is available + String fallbackVersion = findCompatibleInstalledVersion( + alternativeDirFile); + if (fallbackVersion != null) { + getLogger().debug("Using existing Node {} instead of installing {}", + fallbackVersion, nodeVersion); + versionToUse = fallbackVersion; + nodeExecutable = getNodeExecutableForVersion(alternativeDirFile, + versionToUse); + return createActiveInstallation(nodeExecutable, versionToUse, + alternativeDirFile); + } + + // No suitable version found, install the requested version + getLogger().info("Installing Node {} to {}", nodeVersion, + alternativeDir); + try { + nodeInstaller.setNodeVersion(nodeVersion); + nodeInstaller.install(); + nodeExecutable = getNodeExecutableForVersion(alternativeDirFile, + nodeVersion); + return createActiveInstallation(nodeExecutable, nodeVersion, + alternativeDirFile); + } catch (InstallationException e) { + throw new IllegalStateException("Failed to install Node", e); + } + } + + private ActiveNodeInstallation createActiveInstallation(File nodeExecutable, + String version, File installDir) { + String nodePath = nodeExecutable.exists() + ? nodeExecutable.getAbsolutePath() + : null; + if (nodePath == null) { + throw new IllegalStateException( + "Node installation failed - executable not found at " + + nodeExecutable); + } + + String npmCliScript = getNpmCliScriptPath(installDir, version); + if (npmCliScript == null) { + String versionedPath = "node-v" + + (version.startsWith("v") ? version.substring(1) + : version); + boolean isWindows = FrontendUtils.isWindows(); + String expectedNpmPath = isWindows + ? versionedPath + "\\node_modules\\npm\\bin\\npm-cli.js" + : versionedPath + "/lib/node_modules/npm/bin/npm-cli.js"; + File expectedNpmFile = new File(installDir, expectedNpmPath); + throw new IllegalStateException( + "npm-cli.js not found at expected location: " + + expectedNpmFile.getAbsolutePath()); + } + + String npmVersion; + try { + npmVersion = FrontendUtils + .getVersion("npm", + List.of(nodePath, npmCliScript, "--version")) + .getFullVersion(); + } catch (UnknownVersionException e) { + getLogger().debug("Could not determine npm version", e); + npmVersion = "unknown"; + } + + return new ActiveNodeInstallation(nodePath, version, npmCliScript, + npmVersion); + } + + /** + * Scans the install directory for installed Node.js versions and returns + * the newest one that is supported. + * + * @param installDir + * the installation directory to scan + * @return the version string (e.g., "v24.10.0") of the best available + * version, or null if none found + */ + private String findCompatibleInstalledVersion(File installDir) { + if (!installDir.exists() || !installDir.isDirectory()) { + return null; + } + + File[] nodeDirs = installDir.listFiles(file -> file.isDirectory() + && file.getName().startsWith("node-v")); + + if (nodeDirs == null || nodeDirs.length == 0) { + return null; + } + + FrontendVersion bestVersion = null; + String bestVersionString = null; + + for (File nodeDir : nodeDirs) { + String dirName = nodeDir.getName(); + // Extract version from directory name (node-v24.10.0 -> v24.10.0) + String versionString = dirName.substring("node-".length()); + + try { + FrontendVersion version = new FrontendVersion(versionString); + + // Skip versions older than minimum auto-installed version + if (version.isOlderThan( + FrontendTools.MINIMUM_AUTO_INSTALLED_NODE)) { + getLogger().debug( + "Skipping {} - older than minimum auto-installed {}", + versionString, + FrontendTools.MINIMUM_AUTO_INSTALLED_NODE + .getFullVersion()); + continue; + } + + // Skip versions with major version higher than maximum + // supported + if (version + .getMajorVersion() > FrontendTools.MAX_SUPPORTED_NODE_MAJOR_VERSION) { + getLogger().debug( + "Skipping {} - major version {} is newer than maximum supported {}", + versionString, version.getMajorVersion(), + FrontendTools.MAX_SUPPORTED_NODE_MAJOR_VERSION); + continue; + } + + // Verify the node executable actually exists + File nodeExecutable = getNodeExecutableForVersion(installDir, + versionString); + if (!nodeExecutable.exists()) { + getLogger().debug( + "Skipping {} - executable not found at {}", + versionString, nodeExecutable); + continue; + } + + // Keep the newest version + if (bestVersion == null || version.isNewerThan(bestVersion)) { + bestVersion = version; + bestVersionString = versionString; + } + } catch (NumberFormatException e) { + getLogger().debug("Could not parse version from directory: {}", + dirName); + } + } + + return bestVersionString; + } + + /** + * Gets the node executable path for a specific version. + * + * @param installDir + * the installation directory + * @param version + * the version string (e.g., "v24.10.0") + * @return the File pointing to the node executable + */ + private File getNodeExecutableForVersion(File installDir, String version) { + String versionedPath = "node-v" + + (version.startsWith("v") ? version.substring(1) : version); + boolean isWindows = FrontendUtils.isWindows(); + String nodeExecutable = isWindows ? versionedPath + "\\node.exe" + : versionedPath + "/bin/node"; + return new File(installDir, nodeExecutable); + } + + /** + * Gets the npm-cli.js script path for a specific version. + * + * @param installDir + * the installation directory + * @param version + * the version string (e.g., "v24.10.0") + * @return the absolute path to npm-cli.js, or null if not found + */ + private String getNpmCliScriptPath(File installDir, String version) { + String versionedPath = "node-v" + + (version.startsWith("v") ? version.substring(1) : version); + boolean isWindows = FrontendUtils.isWindows(); + String npmPath = isWindows + ? versionedPath + "\\node_modules\\npm\\bin\\npm-cli.js" + : versionedPath + "/lib/node_modules/npm/bin/npm-cli.js"; + File npmCliScript = new File(installDir, npmPath); + return npmCliScript.exists() ? npmCliScript.getAbsolutePath() : null; + } + + private Logger getLogger() { + return LoggerFactory.getLogger(NodeResolver.class); + } +} diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java index 4498dc3f6e1..2629baf0efc 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java @@ -120,8 +120,6 @@ public class Options implements Serializable { private URI nodeDownloadRoot = URI .create(Platform.guess().getNodeDownloadRoot()); - private boolean nodeAutoUpdate = false; - private Lookup lookup; /** @@ -620,19 +618,6 @@ public boolean isBundleBuild() { return bundleBuild; } - /** - * Sets whether it is fine to automatically update the alternate node - * installation if installed version is older than the current default. - * - * @param update - * true to update alternate node when used - * @return the builder - */ - public Options setNodeAutoUpdate(boolean update) { - this.nodeAutoUpdate = update; - return this; - } - /** * Set the java resources folder to be checked for feature file. *

@@ -852,10 +837,6 @@ public URI getNodeDownloadRoot() { return nodeDownloadRoot; } - public boolean isNodeAutoUpdate() { - return nodeAutoUpdate; - } - /** * Gets the lookup instance to use for internal lookups. * diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskRunDevBundleBuild.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskRunDevBundleBuild.java index cf82b55ea2b..d7defec3362 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskRunDevBundleBuild.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskRunDevBundleBuild.java @@ -139,7 +139,6 @@ private void runFrontendBuildTool(String toolName, String packageName, settings.setNodeDownloadRoot(options.getNodeDownloadRoot()); settings.setForceAlternativeNode(options.isRequireHomeNodeExec()); settings.setUseGlobalPnpm(options.isUseGlobalPnpm()); - settings.setAutoUpdate(options.isNodeAutoUpdate()); settings.setNodeVersion(options.getNodeVersion()); settings.setIgnoreVersionChecks( options.isFrontendIgnoreVersionChecks()); @@ -163,12 +162,7 @@ private void runFrontendBuildTool(String toolName, String packageName, toolName, buildExecutable.getAbsolutePath())); } - String nodePath; - if (options.isRequireHomeNodeExec()) { - nodePath = frontendTools.forceAlternativeNodeExecutable(); - } else { - nodePath = frontendTools.getNodeExecutable(); - } + String nodePath = frontendTools.getNodeExecutable(); List command = new ArrayList<>(); command.add(nodePath); diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java index 6d10186e4d8..8f6fa8876f9 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java @@ -229,12 +229,10 @@ private void runNpmInstall() throws ExecutionFailedException { settings.setNodeDownloadRoot(options.getNodeDownloadRoot()); settings.setForceAlternativeNode(options.isRequireHomeNodeExec()); settings.setUseGlobalPnpm(options.isUseGlobalPnpm()); - settings.setAutoUpdate(options.isNodeAutoUpdate()); settings.setNodeVersion(options.getNodeVersion()); settings.setIgnoreVersionChecks( options.isFrontendIgnoreVersionChecks()); FrontendTools tools = new FrontendTools(settings); - tools.validateNodeAndNpmVersion(); if (options.isEnablePnpm()) { try { @@ -252,7 +250,7 @@ private void runNpmInstall() throws ExecutionFailedException { try { if (options.isRequireHomeNodeExec()) { - tools.forceAlternativeNodeExecutable(); + tools.getNodeExecutable(); } if (options.isEnableBun()) { npmExecutable = tools.getBunExecutable(); diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/installer/NodeInstaller.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/installer/NodeInstaller.java index 6392601b14c..34402aee014 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/installer/NodeInstaller.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/installer/NodeInstaller.java @@ -22,7 +22,6 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.StandardCopyOption; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -53,14 +52,10 @@ */ public class NodeInstaller { - public static final String INSTALL_PATH = "/node"; + public static final String INSTALL_PATH_PREFIX = "/node"; public static final String SHA_SUMS_FILE = "SHASUMS256.txt"; - private static final String NODE_WINDOWS = INSTALL_PATH.replaceAll("/", - "\\\\") + "\\node.exe"; - private static final String NODE_DEFAULT = INSTALL_PATH + "/node"; - public static final String PROVIDED_VERSION = "provided"; private static final int MAX_DOWNLOAD_ATTEMPS = 5; @@ -203,7 +198,9 @@ private boolean npmProvided() throws InstallationException { } /** - * Install node and npm. + * Install node and npm. This method unconditionally downloads and installs + * the specified node version without checking if it already exists. Use + * NodeResolver to check for existing installations before calling this. * * @throws InstallationException * exception thrown when installation fails @@ -216,10 +213,6 @@ public void install() throws InstallationException { nodeDownloadRoot = URI.create(platform.getNodeDownloadRoot()); } - if (nodeIsAlreadyInstalled()) { - return; - } - getLogger().info("Installing node version {}", nodeVersion); if (!nodeVersion.startsWith("v")) { getLogger().warn( @@ -233,29 +226,6 @@ public void install() throws InstallationException { } } - private boolean nodeIsAlreadyInstalled() throws InstallationException { - File nodeFile = getNodeExecutable(); - if (nodeFile.exists()) { - - List nodeVersionCommand = new ArrayList<>(); - nodeVersionCommand.add(nodeFile.toString()); - nodeVersionCommand.add("--version"); - String version = getVersion("Node", nodeVersionCommand) - .getFullVersion(); - - if (version.equals(nodeVersion)) { - getLogger().info("Node {} is already installed.", version); - return true; - } else { - getLogger().info( - "Node {} was installed, but we need version {}", - version, nodeVersion); - return false; - } - } - return false; - } - private void installNode(InstallData data) throws InstallationException { try { @@ -424,7 +394,14 @@ public String getInstallDirectory() { } private File getInstallDirectoryFile() { - return new File(installDirectory, INSTALL_PATH); + return new File(installDirectory, getVersionedInstallPath()); + } + + private String getVersionedInstallPath() { + if (nodeVersion == null || PROVIDED_VERSION.equals(nodeVersion)) { + return INSTALL_PATH_PREFIX; + } + return INSTALL_PATH_PREFIX + "-" + nodeVersion; } private File getNodeInstallDirectory() { @@ -627,17 +604,6 @@ private void verifyArchive(File archive) } } - /** - * Get node executable file. - * - * @return node executable - */ - private File getNodeExecutable() { - String nodeExecutable = platform.isWindows() ? NODE_WINDOWS - : NODE_DEFAULT; - return new File(installDirectory + nodeExecutable); - } - private static FrontendVersion getVersion(String tool, List versionCommand) throws InstallationException { try { diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTest.java index 09347e34b2f..03d42f7776a 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTest.java @@ -37,7 +37,6 @@ import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; import org.apache.commons.compress.utils.Lists; import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; import org.junit.Assert; import org.junit.Assume; import org.junit.Before; @@ -49,14 +48,13 @@ import org.slf4j.LoggerFactory; import com.vaadin.flow.function.SerializableSupplier; -import com.vaadin.flow.internal.Pair; import com.vaadin.flow.server.frontend.installer.Platform; import com.vaadin.flow.server.frontend.installer.ProxyConfig; import com.vaadin.flow.testcategory.SlowTests; import com.vaadin.flow.testutil.FrontendStubs; -import static com.vaadin.flow.server.frontend.FrontendTools.NPM_BIN_PATH; import static com.vaadin.flow.testutil.FrontendStubs.createStubNode; +import static com.vaadin.flow.testutil.FrontendStubs.resetFrontendToolsNodeCache; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; @@ -66,8 +64,6 @@ @Category(SlowTests.class) public class FrontendToolsTest { - private static final String SUPPORTED_NODE_BUT_OLDER_THAN_AUTOINSTALLED = "24.0.0"; - public static final String DEFAULT_NODE = FrontendUtils.isWindows() ? "node\\node.exe" : "node/node"; @@ -96,7 +92,9 @@ public class FrontendToolsTest { private FrontendToolsSettings settings; @Before - public void setup() throws IOException { + public void setup() throws Exception { + // Reset static state to ensure clean test isolation + resetFrontendToolsNodeCache(); baseDir = tmpDir.newFolder().getAbsolutePath(); vaadinHomeDir = tmpDir.newFolder().getAbsolutePath(); settings = new FrontendToolsSettings(baseDir, () -> vaadinHomeDir); @@ -106,8 +104,10 @@ public void setup() throws IOException { @Test public void installNode_NodeIsInstalledToTargetDirectory() throws FrontendUtils.UnknownVersionException { - String nodeExecutable = tools - .installNode(FrontendTools.DEFAULT_NODE_VERSION, null); + // Force alternative node to install and set up activeNodeInstallation + settings.setForceAlternativeNode(true); + tools = new FrontendTools(settings); + String nodeExecutable = tools.getNodeExecutable(); Assert.assertNotNull(nodeExecutable); List nodeVersionCommand = new ArrayList<>(); @@ -120,12 +120,9 @@ public void installNode_NodeIsInstalledToTargetDirectory() .getFullVersion(), node.getFullVersion()); - settings.setBaseDir(vaadinHomeDir); - settings.setAlternativeDirGetter(null); - - FrontendTools newTools = new FrontendTools(settings); + // Now test npm with the installed node List npmVersionCommand = new ArrayList<>( - newTools.getNpmExecutable()); + tools.getNpmExecutable()); npmVersionCommand.add("--version"); FrontendVersion npm = FrontendUtils.getVersion("npm", npmVersionCommand); @@ -151,90 +148,11 @@ public void nodeIsBeingLocated_updateTooOldNode_NodeInstalledToTargetDirectoryIs updatedNodeVersion.getFullVersion()); } - @Test - public void nodeIsBeingLocated_supportedNodeInstalled_autoUpdateFalse_NodeNotUpdated() - throws FrontendUtils.UnknownVersionException { - settings.setAutoUpdate(false); - FrontendVersion updatedNodeVersion = getUpdatedAlternativeNodeVersion( - SUPPORTED_NODE_BUT_OLDER_THAN_AUTOINSTALLED, - () -> tools.getNodeExecutable()); - - Assert.assertEquals( - "Locate Node version: Node version updated even if it should not have been touched.", - SUPPORTED_NODE_BUT_OLDER_THAN_AUTOINSTALLED, - updatedNodeVersion.getFullVersion()); - } - - @Test - public void nodeIsBeingLocated_supportedNodeInstalled_autoUpdateTrue_NodeUpdated() - throws FrontendUtils.UnknownVersionException { - FrontendVersion updatedNodeVersion = getUpdatedAlternativeNodeVersion( - SUPPORTED_NODE_BUT_OLDER_THAN_AUTOINSTALLED, - () -> tools.getNodeExecutable()); - - Assert.assertEquals( - "Locate Node version: Node version was not auto updated.", - new FrontendVersion(FrontendTools.DEFAULT_NODE_VERSION) - .getFullVersion(), - updatedNodeVersion.getFullVersion()); - } - - @Test - public void nodeIsBeingLocated_unsupportedNodeInstalled_defaultNodeVersionInstalledToAlternativeDirectory() - throws FrontendUtils.UnknownVersionException, IOException { - Assume.assumeFalse( - "Skipping test on windows until a fake node.exe that isn't caught by Window defender can be created.", - FrontendUtils.isWindows()); - // Unsupported node version - FrontendStubs.ToolStubInfo nodeStub = FrontendStubs.ToolStubInfo - .builder(FrontendStubs.Tool.NODE).withVersion("8.9.3").build(); - FrontendStubs.ToolStubInfo npmStub = FrontendStubs.ToolStubInfo.none(); - createStubNode(nodeStub, npmStub, baseDir); - - List nodeVersionCommand = new ArrayList<>(); - nodeVersionCommand.add(tools.getNodeExecutable()); - nodeVersionCommand.add("--version"); - FrontendVersion usedNodeVersion = FrontendUtils.getVersion("node", - nodeVersionCommand); - - Assert.assertEquals( - "Locate unsupported Node version: Default Node version was not used.", - new FrontendVersion(FrontendTools.DEFAULT_NODE_VERSION) - .getFullVersion(), - usedNodeVersion.getFullVersion()); - } - - @Test - public void nodeIsBeingLocated_unsupportedNodeInstalled_fallbackToNodeInstalledToAlternativeDirectory() - throws IOException, FrontendUtils.UnknownVersionException { - Assume.assumeFalse( - "Skipping test on windows until a fake node.exe that isn't caught by Window defender can be created.", - FrontendUtils.isWindows()); - // Unsupported node version - FrontendStubs.ToolStubInfo nodeStub = FrontendStubs.ToolStubInfo - .builder(FrontendStubs.Tool.NODE).withVersion("8.9.3").build(); - FrontendStubs.ToolStubInfo npmStub = FrontendStubs.ToolStubInfo.none(); - createStubNode(nodeStub, npmStub, baseDir); - - tools.installNode(FrontendTools.DEFAULT_NODE_VERSION, null); - - List nodeVersionCommand = new ArrayList<>(); - nodeVersionCommand.add(tools.getNodeExecutable()); - nodeVersionCommand.add("--version"); - FrontendVersion usedNodeVersion = FrontendUtils.getVersion("node", - nodeVersionCommand); - - Assert.assertEquals( - "Locate unsupported Node version: Expecting Node in alternative directory to be used, but was not.", - FrontendTools.DEFAULT_NODE_VERSION.replace("v", ""), - usedNodeVersion.getFullVersion()); - } - @Test public void forceAlternativeDirectory_updateTooOldNode_NodeInstalledToTargetDirectoryIsUpdated() throws FrontendUtils.UnknownVersionException { FrontendVersion updatedNodeVersion = getUpdatedAlternativeNodeVersion( - "7.7.3", () -> tools.forceAlternativeNodeExecutable()); + "7.7.3", () -> tools.getNodeExecutable()); Assert.assertEquals( "Failed to update the old Node version when alternative directory forced", @@ -243,34 +161,6 @@ public void forceAlternativeDirectory_updateTooOldNode_NodeInstalledToTargetDire updatedNodeVersion.getFullVersion()); } - @Test - public void forceAlternativeDirectory_supportedNodeInstalled_autoUpdateFalse_NodeNotUpdated() - throws FrontendUtils.UnknownVersionException { - settings.setAutoUpdate(false); - FrontendVersion updatedNodeVersion = getUpdatedAlternativeNodeVersion( - SUPPORTED_NODE_BUT_OLDER_THAN_AUTOINSTALLED, - () -> tools.forceAlternativeNodeExecutable()); - - Assert.assertEquals( - "Force alternative directory: Node version updated even if it should not have been touched.", - SUPPORTED_NODE_BUT_OLDER_THAN_AUTOINSTALLED, - updatedNodeVersion.getFullVersion()); - } - - @Test - public void forceAlternativeDirectory_supportedNodeInstalled_autoUpdateTrue_NodeUpdated() - throws FrontendUtils.UnknownVersionException { - FrontendVersion updatedNodeVersion = getUpdatedAlternativeNodeVersion( - SUPPORTED_NODE_BUT_OLDER_THAN_AUTOINSTALLED, - () -> tools.forceAlternativeNodeExecutable()); - - Assert.assertEquals( - "Force alternative directory: Node version was not auto updated.", - new FrontendVersion(FrontendTools.DEFAULT_NODE_VERSION) - .getFullVersion(), - updatedNodeVersion.getFullVersion()); - } - private FrontendVersion getUpdatedAlternativeNodeVersion( String oldNodeVersion, SerializableSupplier nodeUpdateCommand) @@ -279,7 +169,9 @@ private FrontendVersion getUpdatedAlternativeNodeVersion( tools = new FrontendTools(settings); String toBeInstalled = "v" + oldNodeVersion; - String nodeExecutable = tools.installNode(toBeInstalled, null); + String nodeExecutable = FrontendToolsTestHelper.installNode( + new File(vaadinHomeDir), tools.getProxies(), toBeInstalled, + null); Assert.assertNotNull(nodeExecutable); List nodeVersionCommand = new ArrayList<>(); @@ -323,6 +215,9 @@ private void prepareNodeDownloadableZipAt(String baseDir, String version) zipOutputStream.putNextEntry( new ZipEntry(prefix + "/node_modules/npm/bin/npm.cmd")); zipOutputStream.closeEntry(); + zipOutputStream.putNextEntry(new ZipEntry( + prefix + "/node_modules/npm/bin/npm-cli.js")); + zipOutputStream.closeEntry(); } } else { try (OutputStream fo = Files.newOutputStream(tempArchive); @@ -343,6 +238,11 @@ private void prepareNodeDownloadableZipAt(String baseDir, String version) new File(prefix + "/lib/node_modules/npm/bin/npm.cmd"), prefix + "/lib/node_modules/npm/bin/npm.cmd")); o.closeArchiveEntry(); + o.putArchiveEntry(o.createArchiveEntry( + new File(prefix + + "/lib/node_modules/npm/bin/npm-cli.js"), + prefix + "/lib/node_modules/npm/bin/npm-cli.js")); + o.closeArchiveEntry(); } } } @@ -356,7 +256,9 @@ public void installNodeFromFileSystem_NodeIsInstalledToTargetDirectory() String nodeExecutable = installNodeToTempFolder(); Assert.assertNotNull(nodeExecutable); - String npmInstallPath = NPM_BIN_PATH + "npm"; + // Check npm in version-specific directory + String npmInstallPath = getVersionedNpmBinPath( + FrontendTools.DEFAULT_NODE_VERSION) + "npm"; Assert.assertTrue("npm should have been copied to node_modules", new File(vaadinHomeDir, npmInstallPath).exists()); @@ -365,19 +267,25 @@ public void installNodeFromFileSystem_NodeIsInstalledToTargetDirectory() @Test public void installNodeFromFileSystem_ForceAlternativeNodeExecutableInstallsToTargetDirectory() throws Exception { + String testVersion = "v12.10.0"; + String npmPath = getVersionedNpmBinPath(testVersion) + "npm"; Assert.assertFalse("npm should not yet be present", - new File(vaadinHomeDir, NPM_BIN_PATH + "npm").exists()); + new File(vaadinHomeDir, npmPath).exists()); settings.setNodeDownloadRoot(new File(baseDir).toURI()); - settings.setNodeVersion("v12.10.0"); + settings.setNodeVersion(testVersion); tools = new FrontendTools(settings); - prepareNodeDownloadableZipAt(baseDir, "v12.10.0"); - tools.forceAlternativeNodeExecutable(); - - String npmInstallPath = NPM_BIN_PATH + "npm"; + prepareNodeDownloadableZipAt(baseDir, testVersion); + tools.getNodeExecutable(); Assert.assertTrue("npm should have been copied to node_modules", - new File(vaadinHomeDir, npmInstallPath).exists()); + new File(vaadinHomeDir, npmPath).exists()); + } + + private String getVersionedNpmBinPath(String nodeVersion) { + return FrontendUtils.isWindows() + ? "node-" + nodeVersion + "/node_modules/npm/bin/" + : "node-" + nodeVersion + "/lib/node_modules/npm/bin/"; } @Test @@ -386,29 +294,26 @@ public void homeNodeIsNotForced_useGlobalNode() createStubNode(true, true, vaadinHomeDir); // Validate the global node to be applicable for testing. - Pair nodeCommands; - if (FrontendUtils.isWindows()) { - nodeCommands = new Pair<>("node.exe", "node/node.exe"); - } else { - nodeCommands = new Pair<>("node", "node/node"); + String nodeCommand = FrontendUtils.isWindows() ? "node.exe" : "node"; + File file = frontendToolsLocator.tryLocateTool(nodeCommand) + .orElse(null); + if (file == null) { + LoggerFactory.getLogger(FrontendToolsTest.class) + .info("No global node found, skipping test"); + return; } - File file = new File(baseDir, nodeCommands.getSecond()); - if (!file.exists()) { - file = frontendToolsLocator.tryLocateTool(nodeCommands.getFirst()) - .orElse(null); - List versionCommand = Lists.newArrayList(); - versionCommand.add(file.getAbsolutePath()); - versionCommand.add("--version"); // NOSONAR - final FrontendVersion installedNodeVersion = FrontendUtils - .getVersion("node", versionCommand); - if (installedNodeVersion - .isOlderThan(FrontendTools.SUPPORTED_NODE_VERSION)) { - LoggerFactory.getLogger(FrontendToolsTest.class).info( - "Global version of node is {} which is older than the supported version {}", - installedNodeVersion.getFullVersion(), - FrontendTools.SUPPORTED_NODE_VERSION.getFullVersion()); - return; - } + List versionCommand = Lists.newArrayList(); + versionCommand.add(file.getAbsolutePath()); + versionCommand.add("--version"); // NOSONAR + final FrontendVersion installedNodeVersion = FrontendUtils + .getVersion("node", versionCommand); + if (installedNodeVersion + .isOlderThan(FrontendTools.SUPPORTED_NODE_VERSION)) { + LoggerFactory.getLogger(FrontendToolsTest.class).info( + "Global version of node is {} which is older than the supported version {}", + installedNodeVersion.getFullVersion(), + FrontendTools.SUPPORTED_NODE_VERSION.getFullVersion()); + return; } assertThat(tools.getNodeExecutable(), containsString("node")); @@ -469,11 +374,15 @@ public void validateNodeAndNpmVersion_pnpmLockIsNotRemoved() @Test(expected = IllegalStateException.class) public void ensureNodeExecutableInHome_vaadinHomeNodeIsAFolder_throws() throws IOException { - File node = new File(vaadinHomeDir, - FrontendUtils.isWindows() ? "node/node.exe" : "node/node"); + // Create a folder where the node binary should be (versioned path) + String version = FrontendTools.DEFAULT_NODE_VERSION; + String nodePath = FrontendUtils.isWindows() + ? "node-" + version + "/node.exe" + : "node-" + version + "/bin/node"; + File node = new File(vaadinHomeDir, nodePath); FileUtils.forceMkdir(node); - tools.forceAlternativeNodeExecutable(); + tools.getNodeExecutable(); } @Test @@ -654,36 +563,27 @@ public synchronized void getProxies_npmrcWithProxySetting_shouldReturnProxiesLis httpsProxy.nonProxyHosts); } - @Test - public void should_useProjectNodeFirst() throws Exception { - Assume.assumeFalse( - "Skipping test on windows until a fake node.exe that isn't caught by Window defender can be created.", - FrontendUtils.isWindows()); - createStubNode(true, true, baseDir); - - assertNodeCommand(() -> baseDir); - } - - @Test - public void should_useProjectNpmFirst() throws Exception { - Assume.assumeFalse( - "Skipping test on windows until a fake node.exe that isn't caught by Window defender can be created.", - FrontendUtils.isWindows()); - createStubNode(false, true, baseDir); - - assertNpmCommand(() -> baseDir); - } - @Test public void forceHomeNode_useHomeNpmFirst() throws Exception { - Assume.assumeFalse( - "Skipping test on windows until a fake node.exe that isn't caught by Window defender can be created.", - FrontendUtils.isWindows()); settings.setForceAlternativeNode(true); + settings.setNodeDownloadRoot(new File(baseDir).toPath().toUri()); tools = new FrontendTools(settings); - createStubNode(true, true, vaadinHomeDir); - assertNpmCommand(() -> vaadinHomeDir); + // Install node to vaadin home dir using the test helper + prepareNodeDownloadableZipAt(baseDir, + FrontendTools.DEFAULT_NODE_VERSION); + String nodeExecutable = FrontendToolsTestHelper.installNode( + new File(vaadinHomeDir), tools.getProxies(), + FrontendTools.DEFAULT_NODE_VERSION, + new File(baseDir).toPath().toUri()); + Assert.assertNotNull(nodeExecutable); + + // Verify that node and npm from vaadin home are being used + assertThat(tools.getNodeExecutable(), containsString("node")); + assertThat(tools.getNodeExecutable(), containsString(vaadinHomeDir)); + List npmExecutable = tools.getNpmExecutable(); + assertThat(npmExecutable.get(0), containsString(vaadinHomeDir)); + assertThat(npmExecutable.get(1), containsString(NPM_CLI_STRING)); } @Test @@ -854,26 +754,6 @@ public void folderIsAcceptableByNpm_npm7_trueForWindows() Assert.assertTrue(accepted); } - @Test - public void getNpmCacheDir_returnsCorrectPath() - throws IOException, FrontendUtils.CommandExecutionException { - FrontendStubs.ToolStubInfo nodeStub = FrontendStubs.ToolStubInfo.none(); - FrontendStubs.ToolStubInfo npmStub = FrontendStubs.ToolStubInfo - .builder(FrontendStubs.Tool.NPM).withCacheDir("/foo/bar") - .build(); - createStubNode(nodeStub, npmStub, baseDir); - - File npmCacheDir = tools.getNpmCacheDir(); - - Assert.assertNotNull(npmCacheDir); - String npmCachePath = npmCacheDir.getPath(); - - Assert.assertEquals("foo/bar", - npmCachePath - .substring(FilenameUtils.getPrefixLength(npmCachePath)) - .replace("\\", "/")); - } - @Test public void getViteExecutable_returnsCorrectPath() throws IOException, FrontendUtils.CommandExecutionException { @@ -888,7 +768,7 @@ public void getViteExecutable_returnsCorrectPath() } """.getBytes()); Files.createDirectories( - projectDir.toPath().resolve("node_modules/vite/")); + projectDir.toPath().resolve("node_modules/vite/bin/")); var vitePackageJson = Files.createFile( projectDir.toPath().resolve("node_modules/vite/package.json")); @@ -901,10 +781,15 @@ public void getViteExecutable_returnsCorrectPath() } } """.getBytes()); + // Create the actual vite.js file for toRealPath() to work + Files.createFile( + projectDir.toPath().resolve("node_modules/vite/bin/vite.js")); var vite = tools.getNpmPackageExecutable("vite", "vite", projectDir); - Assert.assertEquals( - projectDir.toPath().resolve("node_modules/vite/bin/vite.js"), - vite); + // Use toRealPath() to handle symlinks (e.g., /var -> /private/var on + // macOS) + Assert.assertEquals(projectDir.toPath() + .resolve("node_modules/vite/bin/vite.js").toRealPath(), + vite.toRealPath()); } private void assertNpmCommand(Supplier path) throws IOException { @@ -954,7 +839,8 @@ private void installGlobalPnpm(String pnpmVersion) { } private String installNodeToTempFolder() { - return tools.installNode(FrontendTools.DEFAULT_NODE_VERSION, + return FrontendToolsTestHelper.installNode(new File(vaadinHomeDir), + tools.getProxies(), FrontendTools.DEFAULT_NODE_VERSION, new File(baseDir).toPath().toUri()); } @@ -979,4 +865,118 @@ private void doInstallPnpmGlobally(String pnpmVersion, boolean uninstall) { private String getCommand(String name) { return FrontendUtils.isWindows() ? name + ".cmd" : name; } + + /** + * Manual testing utility to demonstrate which Node.js installation will be + * used. + *

+ * The resolution logic uses any installed Node.js >= minimum supported + * version (v24.0.0). If no suitable installation exists, it installs the + * preferred version specified by -DnodeVersion. + *

+ * Usage examples: + *

    + *
  • Test with global node: {@code mvn exec:java + * -Dexec.mainClass="com.vaadin.flow.server.frontend.FrontendToolsTest" + * -Dexec.classpathScope=test}
  • + *
  • Test forcing alternative: {@code mvn exec:java ... + * -Dalternative=true}
  • + *
  • Test with custom preferred version: {@code mvn exec:java ... + * -DnodeVersion=v24.5.0}
  • + *
+ * + * @param args + * command line arguments (not used) + */ + public static void main(String[] args) { + System.out.println("=".repeat(80)); + System.out.println("Node.js Resolution Test"); + System.out.println("=".repeat(80)); + + try { + // Read configuration from system properties + boolean forceAlternative = Boolean.getBoolean("alternative"); + String preferredVersion = System.getProperty("nodeVersion", + FrontendTools.DEFAULT_NODE_VERSION); + String baseDir = System.getProperty("baseDir", + System.getProperty("user.dir")); + + System.out.println("\nConfiguration:"); + System.out.println(" Base directory: " + baseDir); + System.out.println(" Supported version for global: >= " + + FrontendTools.SUPPORTED_NODE_VERSION.getFullVersion()); + System.out + .println(" Minimum auto-installed version (~/.vaadin): >= " + + FrontendTools.MINIMUM_AUTO_INSTALLED_NODE + .getFullVersion()); + System.out.println(" Maximum major version: " + + FrontendTools.MAX_SUPPORTED_NODE_MAJOR_VERSION); + System.out.println(" Preferred version (to install if needed): " + + preferredVersion); + System.out.println(" Force alternative node: " + forceAlternative); + System.out.println(); + + // Create FrontendTools instance + FrontendToolsSettings settings = new FrontendToolsSettings(baseDir, + () -> FrontendUtils.getVaadinHomeDirectory() + .getAbsolutePath()); + settings.setNodeVersion(preferredVersion); + settings.setForceAlternativeNode(forceAlternative); + + FrontendTools tools = new FrontendTools(settings); + + // Get resolved node information + String nodeExecutable = tools.getNodeExecutable(); + String actualVersionUsed = tools.getNodeVersion().getFullVersion(); + String npmVersion = tools.getNpmVersion().getFullVersion(); + + System.out.println("Resolved Node.js installation:"); + System.out.println(" Node executable: " + nodeExecutable); + System.out.println(" Actual version used: " + actualVersionUsed); + System.out.println(" npm version: " + npmVersion); + + // Check if using global or alternative installation + File nodeFile = new File(nodeExecutable); + boolean isGlobal = !nodeFile.getAbsolutePath() + .contains(FrontendUtils.getVaadinHomeDirectory().getName()); + + System.out.println("\nInstallation type: " + + (isGlobal ? "GLOBAL" : "ALTERNATIVE (~/.vaadin)")); + + if (!isGlobal) { + System.out.println(" Location: " + FrontendUtils + .getVaadinHomeDirectory().getAbsolutePath()); + } + + // Try to run node --version to verify it works + System.out.println("\nVerification:"); + try { + List versionCommand = new ArrayList<>(); + versionCommand.add(nodeExecutable); + versionCommand.add("--version"); + FrontendVersion version = FrontendUtils.getVersion("node", + versionCommand); + System.out.println(" ✓ Node executable is working"); + System.out.println( + " ✓ Verified version: " + version.getFullVersion()); + } catch (Exception e) { + System.out.println(" ✗ Failed to verify node executable: " + + e.getMessage()); + } + + System.out.println("\n" + "=".repeat(80)); + System.out.println("Resolution completed successfully"); + System.out.println("=".repeat(80)); + + } catch (Exception e) { + System.err.println("\n" + "=".repeat(80)); + System.err.println("ERROR: Resolution failed"); + System.err.println("=".repeat(80)); + System.err.println("\nException: " + e.getClass().getName()); + System.err.println("Message: " + e.getMessage()); + System.err.println("\nStack trace:"); + e.printStackTrace(System.err); + System.exit(1); + } + } } diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTestHelper.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTestHelper.java new file mode 100644 index 00000000000..29e106bda41 --- /dev/null +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTestHelper.java @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.server.frontend; + +import java.io.File; +import java.net.URI; + +import com.vaadin.flow.server.frontend.installer.InstallationException; +import com.vaadin.flow.server.frontend.installer.NodeInstaller; +import com.vaadin.flow.server.frontend.installer.ProxyConfig; + +/** + * Test helper utilities for FrontendTools tests. + */ +public class FrontendToolsTestHelper { + + /** + * Install node and npm to a specific directory without using NodeResolver. + * This is a test utility that unconditionally installs the specified + * version. + * + * @param installDir + * directory where node should be installed + * @param proxies + * list of proxy configurations + * @param nodeVersion + * node version to install + * @param downloadRoot + * optional download root for downloading node. May be a + * filesystem file or a URL see + * {@link NodeInstaller#setNodeDownloadRoot(URI)}. + * @return node installation path, or null if installation failed + */ + public static String installNode(File installDir, + java.util.List proxies, String nodeVersion, + URI downloadRoot) { + NodeInstaller nodeInstaller = new NodeInstaller(installDir, proxies) + .setNodeVersion(nodeVersion); + if (downloadRoot != null) { + nodeInstaller.setNodeDownloadRoot(downloadRoot); + } + + try { + nodeInstaller.install(); + } catch (InstallationException e) { + throw new IllegalStateException("Failed to install Node", e); + } + + // Compute the path to the installed node executable + String normalizedVersion = nodeVersion.startsWith("v") + ? nodeVersion.substring(1) + : nodeVersion; + String versionedPath = "node-v" + normalizedVersion; + String nodeBin = FrontendUtils.isWindows() + ? versionedPath + "\\node.exe" + : versionedPath + "/bin/node"; + File nodeExecutable = new File(installDir, nodeBin); + return nodeExecutable.exists() ? nodeExecutable.getAbsolutePath() + : null; + } +} diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java index cae0f0b95de..943cad068fc 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java @@ -229,28 +229,6 @@ public void parseValidVersions() { .isEqualOrNewer(requiredVersionTen)); } - @Test - public void validateLargerThan_passesForNewVersion() { - FrontendUtils.validateToolVersion("test", new FrontendVersion("10.0.2"), - new FrontendVersion(10, 0)); - FrontendUtils.validateToolVersion("test", new FrontendVersion("10.1.2"), - new FrontendVersion(10, 0)); - FrontendUtils.validateToolVersion("test", new FrontendVersion("11.0.2"), - new FrontendVersion(10, 0)); - } - - @Test - public void validateLargerThan_throwsForOldVersion() { - try { - FrontendUtils.validateToolVersion("test", - new FrontendVersion(7, 5, 0), new FrontendVersion(10, 0)); - Assert.fail("No exception was thrown for old version"); - } catch (IllegalStateException e) { - Assert.assertTrue(e.getMessage().contains( - "Your installed 'test' version (7.5.0) is too old. Supported versions are 10.0+")); - } - } - @Test public void parseValidToolVersions() throws IOException { Assert.assertEquals("10.11.12", diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/installer/NodeInstallerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/installer/NodeInstallerTest.java index f736dfafe3c..09fdde11b90 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/installer/NodeInstallerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/installer/NodeInstallerTest.java @@ -105,8 +105,10 @@ public void installNodeFromFileSystem_NodeIsInstalledToTargetDirectory() } } - // add a file to node/node_modules_npm that should be cleaned out - File nodeDirectory = new File(targetDir, "node"); + // add a file to node-{version}/node_modules_npm that should be cleaned + // out + String versionedNodeDir = "node-" + FrontendTools.DEFAULT_NODE_VERSION; + File nodeDirectory = new File(targetDir, versionedNodeDir); String nodeModulesPath = platform.isWindows() ? "node_modules" : "lib/node_modules"; File nodeModulesDirectory = new File(nodeDirectory, nodeModulesPath); @@ -133,11 +135,12 @@ public void installNodeFromFileSystem_NodeIsInstalledToTargetDirectory() throw new IllegalStateException("Failed to install Node", e); } - Assert.assertTrue("npm should have been copied to node_modules", - new File(targetDir, "node/" + nodeExec).exists()); + Assert.assertTrue("node should have been installed", + new File(targetDir, versionedNodeDir + "/" + nodeExec) + .exists()); String npmInstallPath = platform.isWindows() - ? "node/node_modules/npm/bin/npm" - : "node/lib/node_modules/npm/bin/npm"; + ? versionedNodeDir + "/node_modules/npm/bin/npm" + : versionedNodeDir + "/lib/node_modules/npm/bin/npm"; Assert.assertTrue("npm should have been copied to node_modules", new File(targetDir, npmInstallPath).exists()); Assert.assertFalse("old npm files should have been removed", @@ -146,4 +149,5 @@ public void installNodeFromFileSystem_NodeIsInstalledToTargetDirectory() "old style node_modules files should have been removed", oldGarbage.exists()); } + } diff --git a/flow-test-generic/src/main/java/com/vaadin/flow/testutil/FrontendStubs.java b/flow-test-generic/src/main/java/com/vaadin/flow/testutil/FrontendStubs.java index 53ed7cff990..5840fe6ed20 100644 --- a/flow-test-generic/src/main/java/com/vaadin/flow/testutil/FrontendStubs.java +++ b/flow-test-generic/src/main/java/com/vaadin/flow/testutil/FrontendStubs.java @@ -377,4 +377,21 @@ private String generateNpmScript(StringBuilder scriptBuilder) { public enum Tool { NODE, NPM } + + /** + * Resets the static node installation cache in FrontendTools using + * reflection. This ensures test isolation by clearing cached node paths + * between test runs. + * + * @throws Exception + * if reflection fails + */ + public static void resetFrontendToolsNodeCache() throws Exception { + Class frontendToolsClass = Class + .forName("com.vaadin.flow.server.frontend.FrontendTools"); + java.lang.reflect.Field activeNodeField = frontendToolsClass + .getDeclaredField("activeNodeInstallation"); + activeNodeField.setAccessible(true); + activeNodeField.set(null, null); + } } diff --git a/flow-tests/test-frontend/test-bun/pom-production.xml b/flow-tests/test-frontend/test-bun/pom-production.xml index ded39ceae11..a34dd2b4e6c 100644 --- a/flow-tests/test-frontend/test-bun/pom-production.xml +++ b/flow-tests/test-frontend/test-bun/pom-production.xml @@ -74,7 +74,6 @@ true true - true diff --git a/flow-tests/test-frontend/test-bun/pom.xml b/flow-tests/test-frontend/test-bun/pom.xml index e57e93c1d95..0e8d0597996 100644 --- a/flow-tests/test-frontend/test-bun/pom.xml +++ b/flow-tests/test-frontend/test-bun/pom.xml @@ -74,7 +74,6 @@ flow-maven-plugin true - true diff --git a/flow-tests/test-frontend/test-npm/pom-production.xml b/flow-tests/test-frontend/test-npm/pom-production.xml index 5722488a59b..db14b6c67bb 100644 --- a/flow-tests/test-frontend/test-npm/pom-production.xml +++ b/flow-tests/test-frontend/test-npm/pom-production.xml @@ -69,7 +69,6 @@ false true - true diff --git a/flow-tests/test-frontend/test-npm/pom.xml b/flow-tests/test-frontend/test-npm/pom.xml index ffe46f13fcd..4842a99690b 100644 --- a/flow-tests/test-frontend/test-npm/pom.xml +++ b/flow-tests/test-frontend/test-npm/pom.xml @@ -79,7 +79,6 @@ flow-maven-plugin true - true diff --git a/flow-tests/test-frontend/test-pnpm/pom-production.xml b/flow-tests/test-frontend/test-pnpm/pom-production.xml index 1aeb02a81fb..e41f500ae3b 100644 --- a/flow-tests/test-frontend/test-pnpm/pom-production.xml +++ b/flow-tests/test-frontend/test-pnpm/pom-production.xml @@ -79,7 +79,6 @@ ${flow.dev.dependencies.folder}/${flow.dev.dependencies.file} true - true true diff --git a/flow-tests/test-frontend/test-pnpm/pom.xml b/flow-tests/test-frontend/test-pnpm/pom.xml index 065083f2b9a..a6e3c8dccee 100644 --- a/flow-tests/test-frontend/test-pnpm/pom.xml +++ b/flow-tests/test-frontend/test-pnpm/pom.xml @@ -78,7 +78,6 @@ flow-maven-plugin true - true true diff --git a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/AbstractDevServerRunner.java b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/AbstractDevServerRunner.java index bf708e8cbd2..fca135bb919 100644 --- a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/AbstractDevServerRunner.java +++ b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/AbstractDevServerRunner.java @@ -380,7 +380,6 @@ protected Process doStartDevServer() { ApplicationConfiguration config = getApplicationConfiguration(); ProcessBuilder processBuilder = new ProcessBuilder() .directory(getProjectRoot()); - frontendTools.validateNodeAndNpmVersion(); List command = getServerStartupCommand(frontendTools); diff --git a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/startup/AbstractDevModeTest.java b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/startup/AbstractDevModeTest.java index b28fd2ab012..9bda4d9c328 100644 --- a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/startup/AbstractDevModeTest.java +++ b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/startup/AbstractDevModeTest.java @@ -65,6 +65,9 @@ public abstract class AbstractDevModeTest { @Before public void setup() throws Exception { + // Reset static node installation cache to ensure test isolation + com.vaadin.flow.testutil.FrontendStubs.resetFrontendToolsNodeCache(); + Field firstMapping = VaadinServlet.class .getDeclaredField("frontendMapping"); firstMapping.setAccessible(true); diff --git a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/startup/DevModeInitializerTestBase.java b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/startup/DevModeInitializerTestBase.java index f9dd81aaa23..1f79763b59a 100644 --- a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/startup/DevModeInitializerTestBase.java +++ b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/startup/DevModeInitializerTestBase.java @@ -81,7 +81,10 @@ public static class VaadinServletSubClass extends VaadinServlet { public void setup() throws Exception { super.setup(); + // Create stub npm (but not node - use real system node) + // The stub npm needs to be in baseDir/node/ for compatibility createStubNode(false, true, baseDir); + devServerConfigFile = createStubDevServer(baseDir); // Prevent TaskRunNpmInstall#cleanUp from deleting node_modules @@ -130,7 +133,9 @@ public void setup() throws Exception { // NodeUpdater.getDefaultDevDependencies FileUtils.write(mainPackageFile, getInitalPackageJson().toString(), "UTF-8"); - devServerConfigFile.createNewFile(); + // Create a minimal valid vite.config.ts that exports an empty + // configuration + FileUtils.write(devServerConfigFile, "export default {}\n", "UTF-8"); FileUtils.forceMkdir(new File(baseDir, "src/main/java")); devModeStartupListener = new DevModeStartupListener();