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,102 @@ 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 (
263+ "Using existing Node {} instead of installing {}" ,
264+ fallbackVersion , nodeVersion );
265+ activeNodeVersion = fallbackVersion ;
266+ return true ;
267+ }
268+
256269 return false ;
257270 }
258271
272+ /**
273+ * Scans the install directory for installed Node.js versions and returns
274+ * the newest one that is supported.
275+ *
276+ * @return the version string (e.g., "v24.10.0") of the best available
277+ * version, or null if none found
278+ */
279+ private String findCompatibleInstalledVersion () {
280+ if (!installDirectory .exists () || !installDirectory .isDirectory ()) {
281+ return null ;
282+ }
283+
284+ File [] nodeDirs = installDirectory .listFiles (file -> file .isDirectory ()
285+ && file .getName ().startsWith ("node-v" ));
286+
287+ if (nodeDirs == null || nodeDirs .length == 0 ) {
288+ return null ;
289+ }
290+
291+ FrontendVersion bestVersion = null ;
292+ String bestVersionString = null ;
293+
294+ for (File nodeDir : nodeDirs ) {
295+ String dirName = nodeDir .getName ();
296+ // Extract version from directory name (node-v24.10.0 -> v24.10.0)
297+ String versionString = dirName .substring ("node-" .length ());
298+
299+ try {
300+ FrontendVersion version = new FrontendVersion (versionString );
301+
302+ // Skip versions older than minimum supported
303+ if (version .isOlderThan (FrontendTools .SUPPORTED_NODE_VERSION )) {
304+ getLogger ().debug (
305+ "Skipping {} - older than minimum supported {}" ,
306+ versionString ,
307+ FrontendTools .SUPPORTED_NODE_VERSION
308+ .getFullVersion ());
309+ continue ;
310+ }
311+
312+ // Verify the node executable actually exists and works
313+ File nodeExecutable = getNodeExecutableForVersion (versionString );
314+ if (!nodeExecutable .exists ()) {
315+ getLogger ().debug (
316+ "Skipping {} - executable not found at {}" ,
317+ versionString , nodeExecutable );
318+ continue ;
319+ }
320+
321+ // Keep the newest version
322+ if (bestVersion == null || version .isNewerThan (bestVersion )) {
323+ bestVersion = version ;
324+ bestVersionString = versionString ;
325+ }
326+ } catch (NumberFormatException e ) {
327+ getLogger ().debug ("Could not parse version from directory: {}" ,
328+ dirName );
329+ }
330+ }
331+
332+ return bestVersionString ;
333+ }
334+
335+ /**
336+ * Gets the node executable path for a specific version.
337+ */
338+ private File getNodeExecutableForVersion (String version ) {
339+ String versionedPath = INSTALL_PATH_PREFIX + "-" + version ;
340+ String nodeExecutable = platform .isWindows ()
341+ ? versionedPath .replaceAll ("/" , "\\ \\ " ) + "\\ node.exe"
342+ : versionedPath + "/bin/node" ;
343+ return new File (installDirectory + nodeExecutable );
344+ }
345+
259346 private void installNode (InstallData data ) throws InstallationException {
260347 try {
261348
@@ -285,6 +372,7 @@ private void installNode(InstallData data) throws InstallationException {
285372 }
286373
287374 getLogger ().info ("Local node installation successful." );
375+ activeNodeVersion = nodeVersion ;
288376 }
289377
290378 private void installNodeUnix (InstallData data )
@@ -424,7 +512,18 @@ public String getInstallDirectory() {
424512 }
425513
426514 private File getInstallDirectoryFile () {
427- return new File (installDirectory , INSTALL_PATH );
515+ return new File (installDirectory , getVersionedInstallPath ());
516+ }
517+
518+ private String getVersionedInstallPath () {
519+ // Use activeNodeVersion if set (fallback version), otherwise use
520+ // requested nodeVersion
521+ String version = activeNodeVersion != null ? activeNodeVersion
522+ : nodeVersion ;
523+ if (version == null || PROVIDED_VERSION .equals (version )) {
524+ return INSTALL_PATH_PREFIX ;
525+ }
526+ return INSTALL_PATH_PREFIX + "-" + version ;
428527 }
429528
430529 private File getNodeInstallDirectory () {
@@ -627,17 +726,6 @@ private void verifyArchive(File archive)
627726 }
628727 }
629728
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-
641729 private static FrontendVersion getVersion (String tool ,
642730 List <String > versionCommand ) throws InstallationException {
643731 try {
0 commit comments