3636import com .vaadin .flow .internal .MessageDigestUtil ;
3737import com .vaadin .flow .internal .Pair ;
3838import com .vaadin .flow .server .frontend .FileIOUtils ;
39+ import com .vaadin .flow .server .frontend .FrontendTools ;
3940import com .vaadin .flow .server .frontend .FrontendUtils ;
4041import com .vaadin .flow .server .frontend .FrontendVersion ;
4142import com .vaadin .frontendtools .installer .ArchiveExtractionException ;
5354 */
5455public class NodeInstaller {
5556
56- public static final String INSTALL_PATH = "/node" ;
57+ public static final String INSTALL_PATH_PREFIX = "/node" ;
5758
5859 public static final String SHA_SUMS_FILE = "SHASUMS256.txt" ;
5960
60- private static final String NODE_WINDOWS = INSTALL_PATH .replaceAll ("/" ,
61- "\\ \\ " ) + "\\ node.exe" ;
62- private static final String NODE_DEFAULT = INSTALL_PATH + "/node" ;
63-
6461 public static final String PROVIDED_VERSION = "provided" ;
6562
6663 private static final int MAX_DOWNLOAD_ATTEMPS = 5 ;
@@ -74,6 +71,11 @@ public class NodeInstaller {
7471
7572 private String npmVersion = PROVIDED_VERSION ;
7673 private String nodeVersion ;
74+ /**
75+ * The actual node version being used. May differ from nodeVersion if a
76+ * compatible fallback version was found.
77+ */
78+ private String activeNodeVersion ;
7779 private URI nodeDownloadRoot ;
7880 private String userName ;
7981 private String password ;
@@ -234,9 +236,9 @@ public void install() throws InstallationException {
234236 }
235237
236238 private boolean nodeIsAlreadyInstalled () throws InstallationException {
237- File nodeFile = getNodeExecutable ();
239+ // First, check if the exact requested version is installed
240+ File nodeFile = getNodeExecutableForVersion (nodeVersion );
238241 if (nodeFile .exists ()) {
239-
240242 List <String > nodeVersionCommand = new ArrayList <>();
241243 nodeVersionCommand .add (nodeFile .toString ());
242244 nodeVersionCommand .add ("--version" );
@@ -245,17 +247,101 @@ private boolean nodeIsAlreadyInstalled() throws InstallationException {
245247
246248 if (version .equals (nodeVersion )) {
247249 getLogger ().info ("Node {} is already installed." , version );
250+ activeNodeVersion = nodeVersion ;
248251 return true ;
249252 } else {
250253 getLogger ().info (
251254 "Node {} was installed, but we need version {}" ,
252255 version , nodeVersion );
253- return false ;
254256 }
255257 }
258+
259+ // Check if any other compatible version is available
260+ String fallbackVersion = findCompatibleInstalledVersion ();
261+ if (fallbackVersion != null ) {
262+ getLogger ().debug ("Using existing Node {} instead of installing {}" ,
263+ fallbackVersion , nodeVersion );
264+ activeNodeVersion = fallbackVersion ;
265+ return true ;
266+ }
267+
256268 return false ;
257269 }
258270
271+ /**
272+ * Scans the install directory for installed Node.js versions and returns
273+ * the newest one that is supported.
274+ *
275+ * @return the version string (e.g., "v24.10.0") of the best available
276+ * version, or null if none found
277+ */
278+ private String findCompatibleInstalledVersion () {
279+ if (!installDirectory .exists () || !installDirectory .isDirectory ()) {
280+ return null ;
281+ }
282+
283+ File [] nodeDirs = installDirectory .listFiles (file -> file .isDirectory ()
284+ && file .getName ().startsWith ("node-v" ));
285+
286+ if (nodeDirs == null || nodeDirs .length == 0 ) {
287+ return null ;
288+ }
289+
290+ FrontendVersion bestVersion = null ;
291+ String bestVersionString = null ;
292+
293+ for (File nodeDir : nodeDirs ) {
294+ String dirName = nodeDir .getName ();
295+ // Extract version from directory name (node-v24.10.0 -> v24.10.0)
296+ String versionString = dirName .substring ("node-" .length ());
297+
298+ try {
299+ FrontendVersion version = new FrontendVersion (versionString );
300+
301+ // Skip versions older than minimum supported
302+ if (version .isOlderThan (FrontendTools .SUPPORTED_NODE_VERSION )) {
303+ getLogger ().debug (
304+ "Skipping {} - older than minimum supported {}" ,
305+ versionString , FrontendTools .SUPPORTED_NODE_VERSION
306+ .getFullVersion ());
307+ continue ;
308+ }
309+
310+ // Verify the node executable actually exists and works
311+ File nodeExecutable = getNodeExecutableForVersion (
312+ versionString );
313+ if (!nodeExecutable .exists ()) {
314+ getLogger ().debug (
315+ "Skipping {} - executable not found at {}" ,
316+ versionString , nodeExecutable );
317+ continue ;
318+ }
319+
320+ // Keep the newest version
321+ if (bestVersion == null || version .isNewerThan (bestVersion )) {
322+ bestVersion = version ;
323+ bestVersionString = versionString ;
324+ }
325+ } catch (NumberFormatException e ) {
326+ getLogger ().debug ("Could not parse version from directory: {}" ,
327+ dirName );
328+ }
329+ }
330+
331+ return bestVersionString ;
332+ }
333+
334+ /**
335+ * Gets the node executable path for a specific version.
336+ */
337+ private File getNodeExecutableForVersion (String version ) {
338+ String versionedPath = INSTALL_PATH_PREFIX + "-" + version ;
339+ String nodeExecutable = platform .isWindows ()
340+ ? versionedPath .replaceAll ("/" , "\\ \\ " ) + "\\ node.exe"
341+ : versionedPath + "/bin/node" ;
342+ return new File (installDirectory + nodeExecutable );
343+ }
344+
259345 private void installNode (InstallData data ) throws InstallationException {
260346 try {
261347
@@ -285,6 +371,7 @@ private void installNode(InstallData data) throws InstallationException {
285371 }
286372
287373 getLogger ().info ("Local node installation successful." );
374+ activeNodeVersion = nodeVersion ;
288375 }
289376
290377 private void installNodeUnix (InstallData data )
@@ -424,7 +511,18 @@ public String getInstallDirectory() {
424511 }
425512
426513 private File getInstallDirectoryFile () {
427- return new File (installDirectory , INSTALL_PATH );
514+ return new File (installDirectory , getVersionedInstallPath ());
515+ }
516+
517+ private String getVersionedInstallPath () {
518+ // Use activeNodeVersion if set (fallback version), otherwise use
519+ // requested nodeVersion
520+ String version = activeNodeVersion != null ? activeNodeVersion
521+ : nodeVersion ;
522+ if (version == null || PROVIDED_VERSION .equals (version )) {
523+ return INSTALL_PATH_PREFIX ;
524+ }
525+ return INSTALL_PATH_PREFIX + "-" + version ;
428526 }
429527
430528 private File getNodeInstallDirectory () {
@@ -627,17 +725,6 @@ private void verifyArchive(File archive)
627725 }
628726 }
629727
630- /**
631- * Get node executable file.
632- *
633- * @return node executable
634- */
635- private File getNodeExecutable () {
636- String nodeExecutable = platform .isWindows () ? NODE_WINDOWS
637- : NODE_DEFAULT ;
638- return new File (installDirectory + nodeExecutable );
639- }
640-
641728 private static FrontendVersion getVersion (String tool ,
642729 List <String > versionCommand ) throws InstallationException {
643730 try {
0 commit comments