From cac73779cc42bc63e8ef6c8d20da8f3bc7a2d74f Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 06:45:24 +0000 Subject: [PATCH 1/9] test: specify excluding @vaadin packages from the minimum age The packages Vaadin publishes are pinned to the version of the platform in use, so the minimum frontend package age blocks an installation that runs during the first day after a Vaadin release: there is no older version to fall back to and the build fails. Specifies with tests how each package manager should be told to exempt those packages, and that the build is warned when the package manager cannot do it. The resolution itself is left unimplemented until the approach is agreed on; the version checks that tell which package managers support excluding are already in place. --- .../flow/server/frontend/FrontendTools.java | 59 +++++++++-- .../server/frontend/TaskRunNpmInstall.java | 47 +++++++++ .../frontend/TaskRunNpmInstallTest.java | 98 +++++++++++++++++++ 3 files changed, 198 insertions(+), 6 deletions(-) diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java index 9e5ffc6e689..747948f9d0f 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java @@ -140,6 +140,19 @@ public class FrontendTools { static final FrontendVersion MIN_NPM_VERSION_FOR_RELEASE_AGE = new FrontendVersion( 11, 10, 0); + // npm 11.17.0 is the first version that supports + // --min-release-age-exclude, which exempts the packages matching a + // minimatch pattern from both --min-release-age and --before. Older + // versions only warn about an unknown configuration and keep blocking + // the packages. + static final FrontendVersion MIN_NPM_VERSION_FOR_RELEASE_AGE_EXCLUDE = new FrontendVersion( + 11, 17, 0); + + // pnpm 10.17.0 is the first version that supports the + // minimumReleaseAgeExclude setting; pnpm 10.16 ignores it. + static final FrontendVersion MIN_PNPM_VERSION_FOR_RELEASE_AGE_EXCLUDE = new FrontendVersion( + 10, 17, 0); + // pnpm 10.16.0 is the first version that supports the // minimumReleaseAge setting used to delay installation of newly // published packages as a supply-chain mitigation. @@ -750,16 +763,50 @@ public FrontendVersion getNpmVersion() throws UnknownVersionException { * @since 25.2 */ public boolean npmSupportsMinReleaseAge(List npmCommand) { - List versionCmd = new ArrayList<>(npmCommand); + return isAtLeast("npm", npmCommand, MIN_NPM_VERSION_FOR_RELEASE_AGE); + } + + /** + * Checks whether the given npm is new enough to know + * {@code --min-release-age-exclude}, which exempts the packages Vaadin + * publishes itself from the minimum frontend package age. + * + * @param npmCommand + * the npm command to invoke for {@code --version} + * @return {@code true} if the installed npm is new enough; {@code false} if + * it is older or its version cannot be determined + */ + boolean npmSupportsMinReleaseAgeExclude(List npmCommand) { + return isAtLeast("npm", npmCommand, + MIN_NPM_VERSION_FOR_RELEASE_AGE_EXCLUDE); + } + + /** + * Checks whether the given pnpm is new enough to know the + * {@code minimumReleaseAgeExclude} setting, which exempts the packages + * Vaadin publishes itself from the minimum frontend package age. + * + * @param pnpmCommand + * the pnpm command to invoke for {@code --version} + * @return {@code true} if the installed pnpm is new enough; {@code false} + * if it is older or its version cannot be determined + */ + boolean pnpmSupportsMinimumReleaseAgeExclude(List pnpmCommand) { + return isAtLeast("pnpm", pnpmCommand, + MIN_PNPM_VERSION_FOR_RELEASE_AGE_EXCLUDE); + } + + private boolean isAtLeast(String tool, List toolCommand, + FrontendVersion required) { + List versionCmd = new ArrayList<>(toolCommand); versionCmd.add("--version"); // NOSONAR try { - FrontendVersion actual = FrontendUtils.getVersion("npm", - versionCmd); - return actual.isEqualOrNewer(MIN_NPM_VERSION_FOR_RELEASE_AGE); + return FrontendUtils.getVersion(tool, versionCmd) + .isEqualOrNewer(required); } catch (UnknownVersionException e) { getLogger().debug( - "Could not determine npm version; falling back to --before for the minimum frontend package age check", - e); + "Could not determine the {} version; assuming it is older than {}", + tool, required.getFullVersion(), e); return false; } } diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java index 7e731c0cf53..2aee0c48b4a 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java @@ -72,6 +72,14 @@ public class TaskRunNpmInstall implements FallibleCommand { */ static final int DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS = 1; + /** + * The package name pattern that is exempt from the minimum frontend package + * age. The packages Vaadin publishes itself are pinned to the version of + * the platform in use, so a project that is built right after a Vaadin + * release has no older version to fall back to. + */ + static final String MINIMUM_FRONTEND_PACKAGE_AGE_EXCLUDE = "@vaadin/*"; + private static final String MODULES_YAML = ".modules.yaml"; private static final String NPM_VALIDATION_FAIL_MESSAGE = "%n%n======================================================================================================" @@ -510,6 +518,45 @@ static Optional resolveMinimumFrontendPackageAgeArgument( npmSupportsMinReleaseAge)); } + /** + * Resolves the install argument that exempts the packages Vaadin publishes + * itself from the minimum frontend package age, so that a project can be + * built with a Vaadin version that was released a moment ago. + *

+ * The intended behavior is specified by the tests of this method: + *

    + *
  • npm 11.17 or newer is passed + * {@code --min-release-age-exclude=@vaadin/*}, which exempts the matching + * packages from both {@code --min-release-age} and {@code --before}
  • + *
  • pnpm 10.17 or newer is passed + * {@code --config.minimum-release-age-exclude=@vaadin/*}
  • + *
  • bun and older npm and pnpm versions cannot exclude packages on the + * command line, so nothing is passed and the build is warned that an + * installation may fail during the first day after a Vaadin release
  • + *
  • nothing is passed and nothing is warned about when the age check is + * disabled, as then no version is blocked to begin with
  • + *
+ * + * @param options + * current build options + * @param tools + * the frontend tools used to read the package manager version + * @param toolCommand + * the npm, pnpm or bun command used for the install + * @param logger + * the logger to report an unsupported package manager to + * @return the install argument, or an empty optional if none should be + * passed + */ + static Optional resolveMinimumFrontendPackageAgeExcludeArgument( + Options options, FrontendTools tools, List toolCommand, + Logger logger) { + // Not implemented yet: how the exclusion should be configured, and + // what to do for a package manager that cannot exclude packages, is + // still being decided + return Optional.empty(); + } + /** * Reads the minimum release age the active package manager resolves from * its own configuration, so that Vaadin does not override it with a command diff --git a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java index 1472dff5388..5e34cd8785d 100644 --- a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java +++ b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java @@ -889,16 +889,114 @@ void resolveMinimumFrontendPackageAge_bun_configurationIsNotRead() { Mockito.anyList(), Mockito.any(), Mockito.any(String[].class)); } + @Test + void minimumFrontendPackageAgeExclude_npm_excludesVaadinPackages() { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + MockLogger logger = new MockLogger(); + + assertEquals("--min-release-age-exclude=@vaadin/*", + resolveMinimumFrontendPackageAgeExcludeArgument( + new MockOptions(npmFolder), tools, logger) + .orElseThrow()); + assertEquals("", logger.getLogs(), + "excluding the packages needs no warning"); + } + + @Test + void minimumFrontendPackageAgeExclude_pnpm_excludesVaadinPackages() { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + MockLogger logger = new MockLogger(); + + assertEquals("--config.minimum-release-age-exclude=@vaadin/*", + resolveMinimumFrontendPackageAgeExcludeArgument( + new MockOptions(npmFolder).withEnablePnpm(true), tools, + logger).orElseThrow()); + } + + @Test + void minimumFrontendPackageAgeExclude_npmTooOld_warnsInsteadOfExcluding() { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + // npm older than 11.17 only warns about an unknown configuration + Mockito.when(tools.npmSupportsMinReleaseAgeExclude(Mockito.anyList())) + .thenReturn(false); + MockLogger logger = new MockLogger(); + + assertFalse(resolveMinimumFrontendPackageAgeExcludeArgument( + new MockOptions(npmFolder), tools, logger).isPresent()); + assertTrue(logger.getLogs().contains("first day"), + "the build should be warned that an installation may fail " + + "during the first day after a Vaadin release, was: " + + logger.getLogs()); + } + + @Test + void minimumFrontendPackageAgeExclude_bun_warnsInsteadOfExcluding() { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + MockLogger logger = new MockLogger(); + + // bun can only exclude packages through bunfig.toml, and only by + // their exact name + assertFalse(resolveMinimumFrontendPackageAgeExcludeArgument( + new MockOptions(npmFolder).withEnableBun(true), tools, logger) + .isPresent()); + assertTrue(logger.getLogs().contains("first day"), + "the build should be warned that an installation may fail " + + "during the first day after a Vaadin release, was: " + + logger.getLogs()); + } + + @Test + void minimumFrontendPackageAgeExclude_ageCheckDisabled_noArgumentOrWarning() { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + MockLogger logger = new MockLogger(); + + // nothing is blocked, so nothing has to be excluded either + assertFalse(resolveMinimumFrontendPackageAgeExcludeArgument( + new MockOptions(npmFolder).withMinimumFrontendPackageAgeDays(0), + tools, logger).isPresent()); + assertEquals("", logger.getLogs()); + } + + @Test + void minimumFrontendPackageAgeExclude_npmrcValue_isStillExcludedFrom() { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), + Mockito.eq(npmFolder), Mockito.eq("min-release-age"))) + .thenReturn(Optional.of("7")); + Options options = new MockOptions(npmFolder); + + // the age npm resolves itself is kept, but the packages Vaadin + // publishes are excluded from it + assertFalse(resolveMinimumFrontendPackageAgeArgument(options, tools) + .isPresent()); + assertEquals("--min-release-age-exclude=@vaadin/*", + resolveMinimumFrontendPackageAgeExcludeArgument(options, tools, + new MockLogger()).orElseThrow()); + } + private FrontendTools mockToolsWithoutMinimumReleaseAge() { FrontendTools tools = Mockito.mock(FrontendTools.class); Mockito.when(tools.npmSupportsMinReleaseAge(Mockito.anyList())) .thenReturn(true); + Mockito.when(tools.npmSupportsMinReleaseAgeExclude(Mockito.anyList())) + .thenReturn(true); + Mockito.when( + tools.pnpmSupportsMinimumReleaseAgeExclude(Mockito.anyList())) + .thenReturn(true); Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), Mockito.any(), Mockito.any(String[].class))) .thenReturn(Optional.empty()); return tools; } + private Optional resolveMinimumFrontendPackageAgeExcludeArgument( + Options options, FrontendTools tools, Logger logger) { + return TaskRunNpmInstall + .resolveMinimumFrontendPackageAgeExcludeArgument(options, tools, + List.of(TaskRunNpmInstall.getToolName(options)), + logger); + } + private Optional resolveMinimumFrontendPackageAgeArgument( Options options, FrontendTools tools) { return TaskRunNpmInstall.resolveMinimumFrontendPackageAgeArgument( From 82d303d32711bd6e61b021c92f87aa557f8aade6 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 06:57:53 +0000 Subject: [PATCH 2/9] feat: exclude @vaadin packages from the minimum frontend package age Passes --min-release-age-exclude=@vaadin/* to npm 11.17 and newer, and --config.minimum-release-age-exclude=@vaadin/* to pnpm 10.17 and newer, so that a project can be built with a Vaadin version that was released a moment ago. The argument is also passed when the age itself comes from the package manager configuration. bun accepts exclusions only as exact package names in a bunfig.toml, and older npm and pnpm versions do not know the setting at all. Those builds are warned that an installation started during the first day after a Vaadin release may fail, and how to turn the age check off. --- .../vaadin/flow/server/frontend/Options.java | 6 +++ .../server/frontend/TaskRunNpmInstall.java | 51 +++++++++++++++++-- .../frontend/TaskRunPnpmInstallTest.java | 20 ++++++++ .../flow/plugin/maven/BuildDevBundleMojo.java | 6 +++ .../flow/plugin/maven/BuildFrontendMojo.java | 6 +++ .../vaadin/flow/server/InitParameters.java | 6 +++ 6 files changed, 91 insertions(+), 4 deletions(-) diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/Options.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/Options.java index 13b0890698c..8cc8b5d5eb2 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/Options.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/Options.java @@ -1218,6 +1218,12 @@ public Options withCommercialBanner(boolean enableCommercialBanner) { * {@link TaskRunNpmInstall#DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS} is * used. The configuration of bun cannot be read, so the default always * applies for it. + *

+ * The packages Vaadin publishes itself ({@code @vaadin/*}) are exempt from + * the check, so that a project can be built right after a Vaadin release. + * Excluding them requires npm ≥ 11.17.0 or pnpm ≥ 10.17.0; bun cannot + * exclude packages on the command line, so with bun an installation may + * fail during the first day after a Vaadin release. * * @param minimumFrontendPackageAgeDays * minimum allowed age in days, {@code 0} to disable the check, diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java index 2aee0c48b4a..926a7146fd7 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java @@ -333,6 +333,10 @@ private void runNpmInstall() throws ExecutionFailedException { resolveMinimumFrontendPackageAgeArgument(options, tools, npmExecutable, logger).ifPresent(npmInstallCommand::add); + // Also passed when the age itself comes from the package manager + // configuration, so that a just released Vaadin version installs + resolveMinimumFrontendPackageAgeExcludeArgument(options, tools, + npmExecutable, logger).ifPresent(npmInstallCommand::add); postinstallCommand.add("run"); postinstallCommand.add("postinstall"); @@ -523,7 +527,6 @@ static Optional resolveMinimumFrontendPackageAgeArgument( * itself from the minimum frontend package age, so that a project can be * built with a Vaadin version that was released a moment ago. *

- * The intended behavior is specified by the tests of this method: *

    *
  • npm 11.17 or newer is passed * {@code --min-release-age-exclude=@vaadin/*}, which exempts the matching @@ -536,6 +539,8 @@ static Optional resolveMinimumFrontendPackageAgeArgument( *
  • nothing is passed and nothing is warned about when the age check is * disabled, as then no version is blocked to begin with
  • *
+ * Only the excluded packages themselves are exempt; their own dependencies + * still have to be old enough. * * @param options * current build options @@ -551,12 +556,50 @@ static Optional resolveMinimumFrontendPackageAgeArgument( static Optional resolveMinimumFrontendPackageAgeExcludeArgument( Options options, FrontendTools tools, List toolCommand, Logger logger) { - // Not implemented yet: how the exclusion should be configured, and - // what to do for a package manager that cannot exclude packages, is - // still being decided + Integer configuredDays = options.getMinimumFrontendPackageAgeDays(); + if (configuredDays != null && configuredDays == 0) { + // no version is blocked, so nothing has to be excluded + return Optional.empty(); + } + if (options.isEnableBun()) { + warnAboutPackagesThatCannotBeExcluded(logger, + "bun accepts exclusions only as exact package names in the 'minimumReleaseAgeExcludes' setting of a bunfig.toml"); + return Optional.empty(); + } + if (options.isEnablePnpm()) { + if (tools.pnpmSupportsMinimumReleaseAgeExclude(toolCommand)) { + return Optional.of("--config.minimum-release-age-exclude=" + + MINIMUM_FRONTEND_PACKAGE_AGE_EXCLUDE); + } + warnAboutPackagesThatCannotBeExcluded(logger, "pnpm older than " + + FrontendTools.MIN_PNPM_VERSION_FOR_RELEASE_AGE_EXCLUDE + .getFullVersion() + + " ignores the 'minimumReleaseAgeExclude' setting"); + return Optional.empty(); + } + if (tools.npmSupportsMinReleaseAgeExclude(toolCommand)) { + return Optional.of("--min-release-age-exclude=" + + MINIMUM_FRONTEND_PACKAGE_AGE_EXCLUDE); + } + warnAboutPackagesThatCannotBeExcluded(logger, "npm older than " + + FrontendTools.MIN_NPM_VERSION_FOR_RELEASE_AGE_EXCLUDE + .getFullVersion() + + " does not know the '--min-release-age-exclude' argument"); return Optional.empty(); } + private static void warnAboutPackagesThatCannotBeExcluded(Logger logger, + String reason) { + logger.warn( + "The packages Vaadin publishes cannot be excluded from the " + + "minimum frontend package age, as {}. Installing a " + + "Vaadin version during the first day after its " + + "release may therefore fail. Upgrade the package " + + "manager, or set the '{}' parameter to 0 to turn " + + "the age check off.", + reason, InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS); + } + /** * Reads the minimum release age the active package manager resolves from * its own configuration, so that Vaadin does not override it with a command diff --git a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunPnpmInstallTest.java b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunPnpmInstallTest.java index 6bf3ce2d8c5..3642442d6d0 100644 --- a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunPnpmInstallTest.java +++ b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunPnpmInstallTest.java @@ -442,6 +442,26 @@ void runPnpmInstall_ciBuild_usesFrozenLockfile() "pnpm install in CI build should not use --no-frozen-lockfile"); } + @Test + void runPnpmInstall_excludesVaadinPackagesFromTheMinimumAge() + throws ExecutionFailedException, IOException { + TaskRunNpmInstall task = createTask(); + getNodeUpdater().modified = true; + + task.execute(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + Mockito.verify(logger).info( + Mockito.eq("using '{}' for frontend package installation"), + captor.capture()); + assertTrue( + captor.getValue().contains( + "--config.minimum-release-age-exclude=@vaadin/*"), + "pnpm install should let the packages Vaadin publishes be " + + "installed regardless of the minimum frontend " + + "package age"); + } + @Override protected String getToolName() { return "pnpm"; 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 389573e7800..f50ddf7aa53 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 @@ -223,6 +223,12 @@ public class BuildDevBundleMojo extends AbstractMojo * {@code npm install} behaves the same way. Only when there is no such * value does the check default to {@code 1} day. The configuration of bun * cannot be read, so the default always applies for it. + *

+ * The packages Vaadin publishes itself ({@code @vaadin/*}) are exempt from + * the check, so that a project can be built right after a Vaadin release. + * Excluding them requires npm ≥ 11.17.0 or pnpm ≥ 10.17.0; bun cannot + * exclude packages on the command line, so with bun an installation may + * fail during the first day after a Vaadin release. */ @Parameter(property = "vaadin." + InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS) diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java index e0909f65171..496e733a4b9 100644 --- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java +++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java @@ -170,6 +170,12 @@ public class BuildFrontendMojo extends FlowModeAbstractMojo * {@code npm install} behaves the same way. Only when there is no such * value does the check default to {@code 1} day. The configuration of bun * cannot be read, so the default always applies for it. + *

+ * The packages Vaadin publishes itself ({@code @vaadin/*}) are exempt from + * the check, so that a project can be built right after a Vaadin release. + * Excluding them requires npm ≥ 11.17.0 or pnpm ≥ 10.17.0; bun cannot + * exclude packages on the command line, so with bun an installation may + * fail during the first day after a Vaadin release. */ @Parameter(property = "vaadin." + InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS) 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 bd91c9a1053..eb039232337 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 @@ -416,6 +416,12 @@ public class InitParameters implements Serializable { * or {@code pnpm-workspace.yaml}) is used, defaulting to {@code 1} day if * there is none. The configuration of bun cannot be read, so the default * always applies for it. + *

+ * The packages Vaadin publishes itself ({@code @vaadin/*}) are exempt from + * the check, so that a project can be built right after a Vaadin release. + * Excluding them requires npm ≥ 11.17.0 or pnpm ≥ 10.17.0; bun cannot + * exclude packages on the command line, so with bun an installation may + * fail during the first day after a Vaadin release. * * @since 25.1.6 */ From ca510388912829d31f1d64f6e376c5513dad6878 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:11:38 +0000 Subject: [PATCH 3/9] fix: merge the @vaadin exclusion into the configured exclusions A command line value replaces the exclusions the package manager resolves from its own configuration instead of adding to them, so a project that excludes its own scope silently lost that exclusion. The configured patterns are now passed along with '@vaadin/*'. The pnpm argument is also passed twice when there is a single pattern: pnpm reads the setting as a list only when the argument occurs more than once, and pnpm 11 excludes every package when the value is a plain string, which turned the age check off altogether. Nothing is excluded and nothing is warned about when no age applies, which now includes the package manager itself being configured with an age of 0. --- .../flow/server/frontend/FrontendTools.java | 43 +++++ .../server/frontend/TaskRunNpmInstall.java | 148 +++++++++++++----- .../server/frontend/FrontendToolsTest.java | 48 ++++++ .../frontend/TaskRunNpmInstallTest.java | 133 +++++++++++----- 4 files changed, 295 insertions(+), 77 deletions(-) diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java index 747948f9d0f..714cffbb829 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java @@ -674,6 +674,49 @@ Optional getConfiguredSetting(List toolCommand, return Optional.empty(); } + /** + * Reads the values the given npm or pnpm command resolves for a + * configuration key that holds a list, such as + * {@code min-release-age-exclude}. + *

+ * Several keys can be given for a setting that the tool spells differently + * depending on its version; the first one that has a value is used. Both + * tools list such a setting as an array, but a single value written into a + * {@code .npmrc} may also arrive as a comma separated string. + * + * @param toolCommand + * the npm or pnpm command to run + * @param workingDirectory + * the directory the configuration is resolved from, so that a + * project {@code .npmrc} is taken into account + * @param keys + * the configuration keys to look for, in order of preference + * @return the configured values, empty if none of the keys has a value or + * the configuration cannot be read + */ + List getConfiguredSettingValues(List toolCommand, + File workingDirectory, String... keys) { + JsonNode config = getResolvedConfiguration(toolCommand, + workingDirectory); + for (String key : keys) { + JsonNode value = config.get(key); + if (value == null || value.isNull()) { + continue; + } + List items = value.isArray() + ? value.valueStream().toList() + : List.of(value); + List values = items.stream() + .flatMap(item -> Stream.of(item.asString().split(","))) + .map(String::trim).filter(entry -> !entry.isEmpty()) + .toList(); + if (!values.isEmpty()) { + return values; + } + } + return List.of(); + } + /** * Reads the configuration the given npm or pnpm command resolves for a * directory by running {@code config list --json}. diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java index 926a7146fd7..8ef17d4052a 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java @@ -331,12 +331,14 @@ private void runNpmInstall() throws ExecutionFailedException { } } - resolveMinimumFrontendPackageAgeArgument(options, tools, npmExecutable, - logger).ifPresent(npmInstallCommand::add); - // Also passed when the age itself comes from the package manager + MinimumFrontendPackageAge minimumAge = resolveMinimumFrontendPackageAge( + options, tools, npmExecutable, logger); + minimumAge.argument().ifPresent(npmInstallCommand::add); + // Also excluded when the age itself comes from the package manager // configuration, so that a just released Vaadin version installs - resolveMinimumFrontendPackageAgeExcludeArgument(options, tools, - npmExecutable, logger).ifPresent(npmInstallCommand::add); + npmInstallCommand.addAll( + resolveMinimumFrontendPackageAgeExcludeArguments(options, tools, + npmExecutable, minimumAge.applies(), logger)); postinstallCommand.add("run"); postinstallCommand.add("postinstall"); @@ -488,15 +490,15 @@ static String getToolName(Options options) { * the npm, pnpm or bun command used for the install * @param logger * the logger to report the resolved source of the value to - * @return the install argument, or an empty optional if none should be - * passed + * @return whether an age applies at all, and the install argument if one + * has to be passed */ - static Optional resolveMinimumFrontendPackageAgeArgument( + static MinimumFrontendPackageAge resolveMinimumFrontendPackageAge( Options options, FrontendTools tools, List toolCommand, Logger logger) { Integer configuredDays = options.getMinimumFrontendPackageAgeDays(); if (configuredDays != null && configuredDays == 0) { - return Optional.empty(); + return new MinimumFrontendPackageAge(false, Optional.empty()); } boolean npmSupportsMinReleaseAge = !options.isEnableBun() && !options.isEnablePnpm() @@ -514,16 +516,49 @@ static Optional resolveMinimumFrontendPackageAgeArgument( + "'{}' parameter to override it.", getToolName(options), packageManagerValue.get(), InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS); - return Optional.empty(); + return new MinimumFrontendPackageAge( + !blocksNothing(packageManagerValue.get()), + Optional.empty()); } days = DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS; } - return Optional.of(getMinimumFrontendPackageAgeArgument(options, days, - npmSupportsMinReleaseAge)); + return new MinimumFrontendPackageAge(true, + Optional.of(getMinimumFrontendPackageAgeArgument(options, days, + npmSupportsMinReleaseAge))); } /** - * Resolves the install argument that exempts the packages Vaadin publishes + * Whether a minimum frontend package age applies to the install, and the + * install argument that makes the package manager apply the age Vaadin + * resolved, when it has to be passed at all. + * + * @param applies + * {@code true} when some version can be blocked for being too + * new, either because of this argument or because of the + * configuration of the package manager + * @param argument + * the install argument to pass, or an empty optional when the + * package manager needs none + */ + record MinimumFrontendPackageAge(boolean applies, + Optional argument) { + } + + /** + * Checks whether a minimum release age the package manager resolved for + * itself blocks nothing, which a value of zero does. The {@code before} + * date npm falls back to is not a number and always blocks something. + */ + private static boolean blocksNothing(String packageManagerValue) { + try { + return Double.parseDouble(packageManagerValue.trim()) == 0; + } catch (NumberFormatException e) { // NOSONAR + return false; + } + } + + /** + * Resolves the install arguments that exempt the packages Vaadin publishes * itself from the minimum frontend package age, so that a project can be * built with a Vaadin version that was released a moment ago. *

@@ -536,56 +571,85 @@ static Optional resolveMinimumFrontendPackageAgeArgument( *

  • bun and older npm and pnpm versions cannot exclude packages on the * command line, so nothing is passed and the build is warned that an * installation may fail during the first day after a Vaadin release
  • - *
  • nothing is passed and nothing is warned about when the age check is - * disabled, as then no version is blocked to begin with
  • + *
  • nothing is passed and nothing is warned about when no age applies, as + * then no version is blocked to begin with
  • * - * Only the excluded packages themselves are exempt; their own dependencies - * still have to be old enough. + * The patterns the package manager is configured with are passed along, as + * a command line value replaces them instead of adding to them. Only the + * excluded packages themselves are exempt; their own dependencies still + * have to be old enough. * * @param options * current build options * @param tools * the frontend tools used to read the package manager version + * and configuration * @param toolCommand * the npm, pnpm or bun command used for the install + * @param ageApplies + * {@code false} when no version is blocked for being too new, in + * which case nothing has to be excluded * @param logger * the logger to report an unsupported package manager to - * @return the install argument, or an empty optional if none should be - * passed + * @return the install arguments, empty if none should be passed */ - static Optional resolveMinimumFrontendPackageAgeExcludeArgument( + static List resolveMinimumFrontendPackageAgeExcludeArguments( Options options, FrontendTools tools, List toolCommand, - Logger logger) { - Integer configuredDays = options.getMinimumFrontendPackageAgeDays(); - if (configuredDays != null && configuredDays == 0) { - // no version is blocked, so nothing has to be excluded - return Optional.empty(); + boolean ageApplies, Logger logger) { + if (!ageApplies) { + return List.of(); } if (options.isEnableBun()) { warnAboutPackagesThatCannotBeExcluded(logger, "bun accepts exclusions only as exact package names in the 'minimumReleaseAgeExcludes' setting of a bunfig.toml"); - return Optional.empty(); + return List.of(); } if (options.isEnablePnpm()) { - if (tools.pnpmSupportsMinimumReleaseAgeExclude(toolCommand)) { - return Optional.of("--config.minimum-release-age-exclude=" - + MINIMUM_FRONTEND_PACKAGE_AGE_EXCLUDE); + if (!tools.pnpmSupportsMinimumReleaseAgeExclude(toolCommand)) { + warnAboutPackagesThatCannotBeExcluded(logger, "pnpm older than " + + FrontendTools.MIN_PNPM_VERSION_FOR_RELEASE_AGE_EXCLUDE + .getFullVersion() + + " ignores the 'minimumReleaseAgeExclude' setting"); + return List.of(); } - warnAboutPackagesThatCannotBeExcluded(logger, "pnpm older than " - + FrontendTools.MIN_PNPM_VERSION_FOR_RELEASE_AGE_EXCLUDE - .getFullVersion() - + " ignores the 'minimumReleaseAgeExclude' setting"); - return Optional.empty(); + List arguments = excludeArguments( + "--config.minimum-release-age-exclude=", + tools.getConfiguredSettingValues(toolCommand, + options.getNpmFolder(), "minimumReleaseAgeExclude", + "minimum-release-age-exclude")); + if (arguments.size() == 1) { + // pnpm reads the setting as a list only when the argument is + // given more than once, and pnpm 11 excludes every package + // when it is a single string instead of a list + arguments = List.of(arguments.get(0), arguments.get(0)); + } + return arguments; } - if (tools.npmSupportsMinReleaseAgeExclude(toolCommand)) { - return Optional.of("--min-release-age-exclude=" - + MINIMUM_FRONTEND_PACKAGE_AGE_EXCLUDE); + if (!tools.npmSupportsMinReleaseAgeExclude(toolCommand)) { + warnAboutPackagesThatCannotBeExcluded(logger, "npm older than " + + FrontendTools.MIN_NPM_VERSION_FOR_RELEASE_AGE_EXCLUDE + .getFullVersion() + + " does not know the '--min-release-age-exclude' argument"); + return List.of(); } - warnAboutPackagesThatCannotBeExcluded(logger, "npm older than " - + FrontendTools.MIN_NPM_VERSION_FOR_RELEASE_AGE_EXCLUDE - .getFullVersion() - + " does not know the '--min-release-age-exclude' argument"); - return Optional.empty(); + return excludeArguments("--min-release-age-exclude=", + tools.getConfiguredSettingValues(toolCommand, + options.getNpmFolder(), "min-release-age-exclude")); + } + + /** + * Builds one install argument per package name pattern that has to be + * excluded, which are the ones the package manager is configured with plus + * the one Vaadin needs. + */ + private static List excludeArguments(String argumentPrefix, + List configuredPatterns) { + List patterns = new ArrayList<>(configuredPatterns); + if (!patterns.contains(MINIMUM_FRONTEND_PACKAGE_AGE_EXCLUDE)) { + patterns.add(MINIMUM_FRONTEND_PACKAGE_AGE_EXCLUDE); + } + return patterns.stream().map(pattern -> argumentPrefix + pattern) + .toList(); } private static void warnAboutPackagesThatCannotBeExcluded(Logger logger, diff --git a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTest.java b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTest.java index 44ac94406f6..4e55fcfc9f2 100644 --- a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTest.java +++ b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTest.java @@ -973,6 +973,54 @@ void getConfiguredSetting_pnpm_readsTheConfigurationWithConfigList() } } + @Test + void getConfiguredSettingValues_listAndCommaSeparatedValue_areRead() + throws CommandExecutionException { + try (MockedStatic frontendUtils = Mockito + .mockStatic(FrontendUtils.class)) { + frontendUtils + .when(() -> FrontendUtils.executeCommand(Mockito.anyList(), + Mockito.any())) + .thenReturn( + "{\"min-release-age-exclude\": [\"@acme/*\", \"lit\"]}"); + + assertEquals(List.of("@acme/*", "lit"), + tools.getConfiguredSettingValues(List.of("npm"), + new File(baseDir), "min-release-age-exclude")); + + // a single value written into an .npmrc may also arrive as a + // comma separated string + frontendUtils + .when(() -> FrontendUtils.executeCommand(Mockito.anyList(), + Mockito.any())) + .thenReturn( + "{\"min-release-age-exclude\": \"@acme/*, lit\"}"); + + assertEquals(List.of("@acme/*", "lit"), + tools.getConfiguredSettingValues(List.of("npm"), + new File(baseDir), "min-release-age-exclude")); + } + } + + @Test + void getConfiguredSettingValues_keyWithoutValue_isEmpty() + throws CommandExecutionException { + try (MockedStatic frontendUtils = Mockito + .mockStatic(FrontendUtils.class)) { + frontendUtils + .when(() -> FrontendUtils.executeCommand(Mockito.anyList(), + Mockito.any())) + .thenReturn( + "{\"min-release-age-exclude\": null, \"omit\": []}"); + + assertEquals(List.of(), + tools.getConfiguredSettingValues(List.of("npm"), + new File(baseDir), "min-release-age-exclude")); + assertEquals(List.of(), tools.getConfiguredSettingValues( + List.of("npm"), new File(baseDir), "omit")); + } + } + @Test void getConfiguredSetting_firstKeyMissing_fallsBackToTheNextOne() throws CommandExecutionException { diff --git a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java index 5e34cd8785d..3e72761fb28 100644 --- a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java +++ b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java @@ -889,28 +889,65 @@ void resolveMinimumFrontendPackageAge_bun_configurationIsNotRead() { Mockito.anyList(), Mockito.any(), Mockito.any(String[].class)); } + @Test + void resolveMinimumFrontendPackageAge_npmrcValueOfZero_noAgeApplies() { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + // npm is configured not to block anything, so neither does Vaadin + Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), + Mockito.eq(npmFolder), Mockito.eq("min-release-age"))) + .thenReturn(Optional.of("0")); + + TaskRunNpmInstall.MinimumFrontendPackageAge minimumAge = TaskRunNpmInstall + .resolveMinimumFrontendPackageAge(new MockOptions(npmFolder), + tools, List.of("npm"), + LoggerFactory.getLogger(TaskRunNpmInstallTest.class)); + + assertFalse(minimumAge.applies()); + assertFalse(minimumAge.argument().isPresent()); + } + @Test void minimumFrontendPackageAgeExclude_npm_excludesVaadinPackages() { FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); MockLogger logger = new MockLogger(); - assertEquals("--min-release-age-exclude=@vaadin/*", - resolveMinimumFrontendPackageAgeExcludeArgument( - new MockOptions(npmFolder), tools, logger) - .orElseThrow()); + assertEquals(List.of("--min-release-age-exclude=@vaadin/*"), + resolveMinimumFrontendPackageAgeExcludeArguments( + new MockOptions(npmFolder), tools, logger)); assertEquals("", logger.getLogs(), "excluding the packages needs no warning"); } @Test - void minimumFrontendPackageAgeExclude_pnpm_excludesVaadinPackages() { + void minimumFrontendPackageAgeExclude_pnpm_excludesVaadinPackagesAsAList() { FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); MockLogger logger = new MockLogger(); - assertEquals("--config.minimum-release-age-exclude=@vaadin/*", - resolveMinimumFrontendPackageAgeExcludeArgument( + // a single occurrence stays a string, which pnpm 11 reads as + // excluding every package + assertEquals( + List.of("--config.minimum-release-age-exclude=@vaadin/*", + "--config.minimum-release-age-exclude=@vaadin/*"), + resolveMinimumFrontendPackageAgeExcludeArguments( new MockOptions(npmFolder).withEnablePnpm(true), tools, - logger).orElseThrow()); + logger)); + } + + @Test + void minimumFrontendPackageAgeExclude_configuredPatterns_areKept() { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + // a command line value replaces the configured list instead of + // adding to it, so the configured patterns are passed along + Mockito.when(tools.getConfiguredSettingValues(Mockito.anyList(), + Mockito.eq(npmFolder), Mockito.eq("min-release-age-exclude"))) + .thenReturn(List.of("@acme/*", "internal-tooling")); + + assertEquals( + List.of("--min-release-age-exclude=@acme/*", + "--min-release-age-exclude=internal-tooling", + "--min-release-age-exclude=@vaadin/*"), + resolveMinimumFrontendPackageAgeExcludeArguments( + new MockOptions(npmFolder), tools, new MockLogger())); } @Test @@ -921,12 +958,26 @@ void minimumFrontendPackageAgeExclude_npmTooOld_warnsInsteadOfExcluding() { .thenReturn(false); MockLogger logger = new MockLogger(); - assertFalse(resolveMinimumFrontendPackageAgeExcludeArgument( - new MockOptions(npmFolder), tools, logger).isPresent()); - assertTrue(logger.getLogs().contains("first day"), - "the build should be warned that an installation may fail " - + "during the first day after a Vaadin release, was: " - + logger.getLogs()); + assertEquals(List.of(), + resolveMinimumFrontendPackageAgeExcludeArguments( + new MockOptions(npmFolder), tools, logger)); + assertWarnsAboutTheFirstDay(logger); + } + + @Test + void minimumFrontendPackageAgeExclude_pnpmTooOld_warnsInsteadOfExcluding() { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + // pnpm 10.16 ignores the setting + Mockito.when( + tools.pnpmSupportsMinimumReleaseAgeExclude(Mockito.anyList())) + .thenReturn(false); + MockLogger logger = new MockLogger(); + + assertEquals(List.of(), + resolveMinimumFrontendPackageAgeExcludeArguments( + new MockOptions(npmFolder).withEnablePnpm(true), tools, + logger)); + assertWarnsAboutTheFirstDay(logger); } @Test @@ -936,24 +987,24 @@ void minimumFrontendPackageAgeExclude_bun_warnsInsteadOfExcluding() { // bun can only exclude packages through bunfig.toml, and only by // their exact name - assertFalse(resolveMinimumFrontendPackageAgeExcludeArgument( - new MockOptions(npmFolder).withEnableBun(true), tools, logger) - .isPresent()); - assertTrue(logger.getLogs().contains("first day"), - "the build should be warned that an installation may fail " - + "during the first day after a Vaadin release, was: " - + logger.getLogs()); + assertEquals(List.of(), + resolveMinimumFrontendPackageAgeExcludeArguments( + new MockOptions(npmFolder).withEnableBun(true), tools, + logger)); + assertWarnsAboutTheFirstDay(logger); } @Test - void minimumFrontendPackageAgeExclude_ageCheckDisabled_noArgumentOrWarning() { + void minimumFrontendPackageAgeExclude_noAgeApplies_noArgumentOrWarning() { FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); MockLogger logger = new MockLogger(); // nothing is blocked, so nothing has to be excluded either - assertFalse(resolveMinimumFrontendPackageAgeExcludeArgument( - new MockOptions(npmFolder).withMinimumFrontendPackageAgeDays(0), - tools, logger).isPresent()); + assertEquals(List.of(), + TaskRunNpmInstall + .resolveMinimumFrontendPackageAgeExcludeArguments( + new MockOptions(npmFolder).withEnableBun(true), + tools, List.of("bun"), false, logger)); assertEquals("", logger.getLogs()); } @@ -969,9 +1020,16 @@ void minimumFrontendPackageAgeExclude_npmrcValue_isStillExcludedFrom() { // publishes are excluded from it assertFalse(resolveMinimumFrontendPackageAgeArgument(options, tools) .isPresent()); - assertEquals("--min-release-age-exclude=@vaadin/*", - resolveMinimumFrontendPackageAgeExcludeArgument(options, tools, - new MockLogger()).orElseThrow()); + assertEquals(List.of("--min-release-age-exclude=@vaadin/*"), + resolveMinimumFrontendPackageAgeExcludeArguments(options, tools, + new MockLogger())); + } + + private void assertWarnsAboutTheFirstDay(MockLogger logger) { + assertTrue(logger.getLogs().contains("first day"), + "the build should be warned that an installation may fail " + + "during the first day after a Vaadin release, was: " + + logger.getLogs()); } private FrontendTools mockToolsWithoutMinimumReleaseAge() { @@ -986,22 +1044,27 @@ private FrontendTools mockToolsWithoutMinimumReleaseAge() { Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), Mockito.any(), Mockito.any(String[].class))) .thenReturn(Optional.empty()); + Mockito.when(tools.getConfiguredSettingValues(Mockito.anyList(), + Mockito.any(), Mockito.any(String[].class))) + .thenReturn(List.of()); return tools; } - private Optional resolveMinimumFrontendPackageAgeExcludeArgument( + private List resolveMinimumFrontendPackageAgeExcludeArguments( Options options, FrontendTools tools, Logger logger) { return TaskRunNpmInstall - .resolveMinimumFrontendPackageAgeExcludeArgument(options, tools, - List.of(TaskRunNpmInstall.getToolName(options)), - logger); + .resolveMinimumFrontendPackageAgeExcludeArguments(options, + tools, List.of(TaskRunNpmInstall.getToolName(options)), + true, logger); } private Optional resolveMinimumFrontendPackageAgeArgument( Options options, FrontendTools tools) { - return TaskRunNpmInstall.resolveMinimumFrontendPackageAgeArgument( - options, tools, List.of("npm"), - LoggerFactory.getLogger(TaskRunNpmInstallTest.class)); + return TaskRunNpmInstall + .resolveMinimumFrontendPackageAge(options, tools, + List.of("npm"), + LoggerFactory.getLogger(TaskRunNpmInstallTest.class)) + .argument(); } @Test From be6b736ad384b5f17ddfff3df9bdc94b1463aae2 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:28:01 +0000 Subject: [PATCH 4/9] fix: tell how to make each package manager exclude the packages The warning no longer suggests turning the age check off. It now ends with the remedy for the package manager in use: upgrading npm to 11.17.0, which Node.js 24.19.0 is the first release to ship, upgrading pnpm to 10.17.0, or, for bun, listing the '@vaadin' packages one by one in the 'minimumReleaseAgeExcludes' setting of a bunfig.toml, as bun matches exact names only. The parameter documentation names the Node.js version as well. --- .../flow/server/frontend/FrontendTools.java | 6 ++++ .../vaadin/flow/server/frontend/Options.java | 7 ++-- .../server/frontend/TaskRunNpmInstall.java | 33 ++++++++++++------- .../frontend/TaskRunNpmInstallTest.java | 6 ++++ .../flow/plugin/maven/BuildDevBundleMojo.java | 7 ++-- .../flow/plugin/maven/BuildFrontendMojo.java | 7 ++-- .../vaadin/flow/server/InitParameters.java | 7 ++-- 7 files changed, 49 insertions(+), 24 deletions(-) diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java index 714cffbb829..a90f9ae6930 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java @@ -148,6 +148,12 @@ public class FrontendTools { static final FrontendVersion MIN_NPM_VERSION_FOR_RELEASE_AGE_EXCLUDE = new FrontendVersion( 11, 17, 0); + // Node.js 24.19.0 is the first release of the supported Node.js line that + // ships npm 11.17.0, which is what a global installation has to be + // upgraded to. The Node.js version Vaadin installs itself is newer. + static final FrontendVersion MIN_NODE_VERSION_FOR_RELEASE_AGE_EXCLUDE = new FrontendVersion( + 24, 19, 0); + // pnpm 10.17.0 is the first version that supports the // minimumReleaseAgeExclude setting; pnpm 10.16 ignores it. static final FrontendVersion MIN_PNPM_VERSION_FOR_RELEASE_AGE_EXCLUDE = new FrontendVersion( diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/Options.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/Options.java index 8cc8b5d5eb2..1c2c2848d8d 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/Options.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/Options.java @@ -1221,9 +1221,10 @@ public Options withCommercialBanner(boolean enableCommercialBanner) { *

    * The packages Vaadin publishes itself ({@code @vaadin/*}) are exempt from * the check, so that a project can be built right after a Vaadin release. - * Excluding them requires npm ≥ 11.17.0 or pnpm ≥ 10.17.0; bun cannot - * exclude packages on the command line, so with bun an installation may - * fail during the first day after a Vaadin release. + * Excluding them requires npm ≥ 11.17.0, which Node.js ≥ 24.19.0 + * ships with, or pnpm ≥ 10.17.0; bun cannot exclude packages on the + * command line, so with bun an installation may fail during the first day + * after a Vaadin release. * * @param minimumFrontendPackageAgeDays * minimum allowed age in days, {@code 0} to disable the check, diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java index 8ef17d4052a..74215ec2593 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java @@ -601,7 +601,9 @@ static List resolveMinimumFrontendPackageAgeExcludeArguments( } if (options.isEnableBun()) { warnAboutPackagesThatCannotBeExcluded(logger, - "bun accepts exclusions only as exact package names in the 'minimumReleaseAgeExcludes' setting of a bunfig.toml"); + "bun accepts exclusions only as exact package names in the 'minimumReleaseAgeExcludes' setting of a bunfig.toml", + "List the '" + MINIMUM_FRONTEND_PACKAGE_AGE_EXCLUDE + + "' packages the project depends on in that setting, spelled out one by one, to get the same result as with npm and pnpm."); return List.of(); } if (options.isEnablePnpm()) { @@ -609,7 +611,11 @@ static List resolveMinimumFrontendPackageAgeExcludeArguments( warnAboutPackagesThatCannotBeExcluded(logger, "pnpm older than " + FrontendTools.MIN_PNPM_VERSION_FOR_RELEASE_AGE_EXCLUDE .getFullVersion() - + " ignores the 'minimumReleaseAgeExclude' setting"); + + " ignores the 'minimumReleaseAgeExclude' setting", + "Upgrade pnpm to " + + FrontendTools.MIN_PNPM_VERSION_FOR_RELEASE_AGE_EXCLUDE + .getFullVersion() + + " or newer."); return List.of(); } List arguments = excludeArguments( @@ -629,7 +635,14 @@ static List resolveMinimumFrontendPackageAgeExcludeArguments( warnAboutPackagesThatCannotBeExcluded(logger, "npm older than " + FrontendTools.MIN_NPM_VERSION_FOR_RELEASE_AGE_EXCLUDE .getFullVersion() - + " does not know the '--min-release-age-exclude' argument"); + + " does not know the '--min-release-age-exclude' argument", + "Upgrade npm to " + + FrontendTools.MIN_NPM_VERSION_FOR_RELEASE_AGE_EXCLUDE + .getFullVersion() + + " or newer, which Node.js " + + FrontendTools.MIN_NODE_VERSION_FOR_RELEASE_AGE_EXCLUDE + .getFullVersion() + + " and newer ship with."); return List.of(); } return excludeArguments("--min-release-age-exclude=", @@ -653,15 +666,11 @@ private static List excludeArguments(String argumentPrefix, } private static void warnAboutPackagesThatCannotBeExcluded(Logger logger, - String reason) { - logger.warn( - "The packages Vaadin publishes cannot be excluded from the " - + "minimum frontend package age, as {}. Installing a " - + "Vaadin version during the first day after its " - + "release may therefore fail. Upgrade the package " - + "manager, or set the '{}' parameter to 0 to turn " - + "the age check off.", - reason, InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS); + String reason, String remedy) { + logger.warn("The packages Vaadin publishes cannot be excluded from the " + + "minimum frontend package age, as {}. Installing a " + + "Vaadin version during the first day after its " + + "release may therefore fail. {}", reason, remedy); } /** diff --git a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java index 3e72761fb28..befcdbdc5ba 100644 --- a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java +++ b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java @@ -962,6 +962,12 @@ void minimumFrontendPackageAgeExclude_npmTooOld_warnsInsteadOfExcluding() { resolveMinimumFrontendPackageAgeExcludeArguments( new MockOptions(npmFolder), tools, logger)); assertWarnsAboutTheFirstDay(logger); + assertTrue( + logger.getLogs().contains("11.17.0") + && logger.getLogs().contains("24.19.0"), + "the warning should name the npm version to upgrade to and " + + "the Node.js version that ships it, was: " + + logger.getLogs()); } @Test 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 f50ddf7aa53..3e1170a68b9 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 @@ -226,9 +226,10 @@ public class BuildDevBundleMojo extends AbstractMojo *

    * The packages Vaadin publishes itself ({@code @vaadin/*}) are exempt from * the check, so that a project can be built right after a Vaadin release. - * Excluding them requires npm ≥ 11.17.0 or pnpm ≥ 10.17.0; bun cannot - * exclude packages on the command line, so with bun an installation may - * fail during the first day after a Vaadin release. + * Excluding them requires npm ≥ 11.17.0, which Node.js ≥ 24.19.0 + * ships with, or pnpm ≥ 10.17.0; bun cannot exclude packages on the + * command line, so with bun an installation may fail during the first day + * after a Vaadin release. */ @Parameter(property = "vaadin." + InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS) diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java index 496e733a4b9..f0c7f3faf01 100644 --- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java +++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java @@ -173,9 +173,10 @@ public class BuildFrontendMojo extends FlowModeAbstractMojo *

    * The packages Vaadin publishes itself ({@code @vaadin/*}) are exempt from * the check, so that a project can be built right after a Vaadin release. - * Excluding them requires npm ≥ 11.17.0 or pnpm ≥ 10.17.0; bun cannot - * exclude packages on the command line, so with bun an installation may - * fail during the first day after a Vaadin release. + * Excluding them requires npm ≥ 11.17.0, which Node.js ≥ 24.19.0 + * ships with, or pnpm ≥ 10.17.0; bun cannot exclude packages on the + * command line, so with bun an installation may fail during the first day + * after a Vaadin release. */ @Parameter(property = "vaadin." + InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS) 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 eb039232337..ce77accd40e 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 @@ -419,9 +419,10 @@ public class InitParameters implements Serializable { *

    * The packages Vaadin publishes itself ({@code @vaadin/*}) are exempt from * the check, so that a project can be built right after a Vaadin release. - * Excluding them requires npm ≥ 11.17.0 or pnpm ≥ 10.17.0; bun cannot - * exclude packages on the command line, so with bun an installation may - * fail during the first day after a Vaadin release. + * Excluding them requires npm ≥ 11.17.0, which Node.js ≥ 24.19.0 + * ships with, or pnpm ≥ 10.17.0; bun cannot exclude packages on the + * command line, so with bun an installation may fail during the first day + * after a Vaadin release. * * @since 25.1.6 */ From 91361cd795d8325f5a9a5a05068613c3149b5cf6 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:30:32 +0000 Subject: [PATCH 5/9] test: pin the pnpm and bun remedies of the exclusion warning Only the npm remedy was asserted, so swapping the other two messages would have gone unnoticed. --- .../flow/server/frontend/TaskRunNpmInstallTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java index befcdbdc5ba..1e6b636b1a4 100644 --- a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java +++ b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java @@ -984,6 +984,9 @@ void minimumFrontendPackageAgeExclude_pnpmTooOld_warnsInsteadOfExcluding() { new MockOptions(npmFolder).withEnablePnpm(true), tools, logger)); assertWarnsAboutTheFirstDay(logger); + assertTrue(logger.getLogs().contains("10.17.0"), + "the warning should name the pnpm version to upgrade to, was: " + + logger.getLogs()); } @Test @@ -998,6 +1001,11 @@ void minimumFrontendPackageAgeExclude_bun_warnsInsteadOfExcluding() { new MockOptions(npmFolder).withEnableBun(true), tools, logger)); assertWarnsAboutTheFirstDay(logger); + assertTrue( + logger.getLogs().contains("minimumReleaseAgeExcludes") + && logger.getLogs().contains("@vaadin/*"), + "the warning should name the bunfig.toml setting to list the " + + "packages in, was: " + logger.getLogs()); } @Test From 441a0e8caff94ec8e2566ca501cb366dafd59c60 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:48:08 +0000 Subject: [PATCH 6/9] fix: keep quiet when a bunfig.toml already excludes the packages A bun build could do nothing to stop the warning, as the exclusion it asks for cannot be seen from the command line. The bunfig.toml of the project is now read, and the warning is skipped when it lists Vaadin packages in 'minimumReleaseAgeExcludes'. Also states the check of the age a package manager resolved for itself the way it is used, instead of negating it at the call site. --- .../server/frontend/TaskRunNpmInstall.java | 52 +++++++++++++++---- .../frontend/TaskRunNpmInstallTest.java | 18 +++++++ 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java index 74215ec2593..d8d1bc5cf02 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java @@ -517,7 +517,7 @@ static MinimumFrontendPackageAge resolveMinimumFrontendPackageAge( getToolName(options), packageManagerValue.get(), InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS); return new MinimumFrontendPackageAge( - !blocksNothing(packageManagerValue.get()), + blocksSomeVersion(packageManagerValue.get()), Optional.empty()); } days = DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS; @@ -546,14 +546,15 @@ record MinimumFrontendPackageAge(boolean applies, /** * Checks whether a minimum release age the package manager resolved for - * itself blocks nothing, which a value of zero does. The {@code before} - * date npm falls back to is not a number and always blocks something. + * itself blocks some version, which every value but zero does. The + * {@code before} date npm falls back to is not a number and always blocks + * something. */ - private static boolean blocksNothing(String packageManagerValue) { + private static boolean blocksSomeVersion(String packageManagerValue) { try { - return Double.parseDouble(packageManagerValue.trim()) == 0; + return Double.parseDouble(packageManagerValue.trim()) != 0; } catch (NumberFormatException e) { // NOSONAR - return false; + return true; } } @@ -570,7 +571,8 @@ private static boolean blocksNothing(String packageManagerValue) { * {@code --config.minimum-release-age-exclude=@vaadin/*} *

  • bun and older npm and pnpm versions cannot exclude packages on the * command line, so nothing is passed and the build is warned that an - * installation may fail during the first day after a Vaadin release
  • + * installation may fail during the first day after a Vaadin release, unless + * a {@code bunfig.toml} of the project lists the packages already *
  • nothing is passed and nothing is warned about when no age applies, as * then no version is blocked to begin with
  • * @@ -600,10 +602,12 @@ static List resolveMinimumFrontendPackageAgeExcludeArguments( return List.of(); } if (options.isEnableBun()) { - warnAboutPackagesThatCannotBeExcluded(logger, - "bun accepts exclusions only as exact package names in the 'minimumReleaseAgeExcludes' setting of a bunfig.toml", - "List the '" + MINIMUM_FRONTEND_PACKAGE_AGE_EXCLUDE - + "' packages the project depends on in that setting, spelled out one by one, to get the same result as with npm and pnpm."); + if (!bunfigExcludesVaadinPackages(options.getNpmFolder(), logger)) { + warnAboutPackagesThatCannotBeExcluded(logger, + "bun accepts exclusions only as exact package names in the 'minimumReleaseAgeExcludes' setting of a bunfig.toml", + "List the '" + MINIMUM_FRONTEND_PACKAGE_AGE_EXCLUDE + + "' packages the project depends on in that setting, spelled out one by one, to get the same result as with npm and pnpm."); + } return List.of(); } if (options.isEnablePnpm()) { @@ -665,6 +669,32 @@ private static List excludeArguments(String argumentPrefix, .toList(); } + /** + * Checks whether a {@code bunfig.toml} next to the {@code package.json} + * already lists packages Vaadin publishes in its + * {@code minimumReleaseAgeExcludes} setting, so that a build that has taken + * care of the exclusion is not warned about it on every run. + *

    + * The file is read as it is, as bun has no command for printing its + * resolved configuration. A {@code bunfig.toml} the project does not + * contain itself, such as the one in the home directory, is not seen. + */ + private static boolean bunfigExcludesVaadinPackages(File npmFolder, + Logger logger) { + File bunfig = new File(npmFolder, "bunfig.toml"); + if (!bunfig.isFile()) { + return false; + } + try { + String content = Files.readString(bunfig.toPath()); + return content.contains("minimumReleaseAgeExcludes") + && content.contains("@vaadin/"); + } catch (IOException | UncheckedIOException e) { + logger.debug("Could not read '{}'", bunfig, e); + return false; + } + } + private static void warnAboutPackagesThatCannotBeExcluded(Logger logger, String reason, String remedy) { logger.warn("The packages Vaadin publishes cannot be excluded from the " diff --git a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java index 1e6b636b1a4..7994d9697c1 100644 --- a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java +++ b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java @@ -1008,6 +1008,24 @@ void minimumFrontendPackageAgeExclude_bun_warnsInsteadOfExcluding() { + "packages in, was: " + logger.getLogs()); } + @Test + void minimumFrontendPackageAgeExclude_bunfigListsThePackages_noWarning() + throws IOException { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + MockLogger logger = new MockLogger(); + Files.writeString(new File(npmFolder, "bunfig.toml").toPath(), """ + [install] + minimumReleaseAgeExcludes = ["@vaadin/react-components"] + """); + + assertEquals(List.of(), + resolveMinimumFrontendPackageAgeExcludeArguments( + new MockOptions(npmFolder).withEnableBun(true), tools, + logger)); + assertEquals("", logger.getLogs(), + "a project that lists the packages itself needs no warning"); + } + @Test void minimumFrontendPackageAgeExclude_noAgeApplies_noArgumentOrWarning() { FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); From 6ece3fbc98e127fd2b5a878e320c40d38b6cdfd0 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:50:41 +0000 Subject: [PATCH 7/9] fix: only skip the bun warning for an actual exclusion of the packages Both strings were looked for anywhere in the bunfig.toml, so a commented out setting or an unrelated mention of a Vaadin package was enough to lose the warning. The package now has to be a value of the setting. --- .../server/frontend/TaskRunNpmInstall.java | 10 ++++++++-- .../frontend/TaskRunNpmInstallTest.java | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java index d8d1bc5cf02..4658c36b98d 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java @@ -80,6 +80,13 @@ public class TaskRunNpmInstall implements FallibleCommand { */ static final String MINIMUM_FRONTEND_PACKAGE_AGE_EXCLUDE = "@vaadin/*"; + /** + * A {@code minimumReleaseAgeExcludes} setting of a {@code bunfig.toml} that + * has a package Vaadin publishes among its values. + */ + private static final Pattern BUNFIG_VAADIN_EXCLUDE = Pattern + .compile("minimumReleaseAgeExcludes\\s*=\\s*\\[[^]]*[\"']@vaadin/"); + private static final String MODULES_YAML = ".modules.yaml"; private static final String NPM_VALIDATION_FAIL_MESSAGE = "%n%n======================================================================================================" @@ -687,8 +694,7 @@ private static boolean bunfigExcludesVaadinPackages(File npmFolder, } try { String content = Files.readString(bunfig.toPath()); - return content.contains("minimumReleaseAgeExcludes") - && content.contains("@vaadin/"); + return BUNFIG_VAADIN_EXCLUDE.matcher(content).find(); } catch (IOException | UncheckedIOException e) { logger.debug("Could not read '{}'", bunfig, e); return false; diff --git a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java index 7994d9697c1..295725b3a9a 100644 --- a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java +++ b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java @@ -1026,6 +1026,26 @@ void minimumFrontendPackageAgeExclude_bunfigListsThePackages_noWarning() "a project that lists the packages itself needs no warning"); } + @Test + void minimumFrontendPackageAgeExclude_bunfigListsOtherPackages_warns() + throws IOException { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + MockLogger logger = new MockLogger(); + // the packages Vaadin publishes are not among the excluded ones, + // mentioning them elsewhere in the file is not enough + Files.writeString(new File(npmFolder, "bunfig.toml").toPath(), """ + [install] + # @vaadin/react-components + minimumReleaseAgeExcludes = ["react"] + """); + + assertEquals(List.of(), + resolveMinimumFrontendPackageAgeExcludeArguments( + new MockOptions(npmFolder).withEnableBun(true), tools, + logger)); + assertWarnsAboutTheFirstDay(logger); + } + @Test void minimumFrontendPackageAgeExclude_noAgeApplies_noArgumentOrWarning() { FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); From a333232743fc68f517e9a355ad7581f09b0d4f54 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 07:06:50 +0000 Subject: [PATCH 8/9] fix: exclude the packages also from an age only the package manager sets With 'minimumFrontendPackageAgeDays' set to 0 Vaadin passes no argument, but an age npm or pnpm resolves from its own configuration still applies to the install. That age is now reported as applying, so the packages Vaadin publishes are excluded from it instead of being blocked silently. The values of a configured exclusion list are no longer split on commas either. Only a comma separated string is, as a list value is complete on its own and may contain a comma of its own in a brace expansion such as '@acme/{ui,core}'. --- .../flow/server/frontend/FrontendTools.java | 17 ++++++------ .../server/frontend/TaskRunNpmInstall.java | 27 +++++++++++++------ .../server/frontend/FrontendToolsTest.java | 19 +++++++++++++ .../frontend/TaskRunNpmInstallTest.java | 24 +++++++++++++++++ 4 files changed, 71 insertions(+), 16 deletions(-) diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java index 1136c00da4c..0b00c162974 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java @@ -688,7 +688,10 @@ Optional getConfiguredSetting(List toolCommand, * Several keys can be given for a setting that the tool spells differently * depending on its version; the first one that has a value is used. Both * tools list such a setting as an array, but a single value written into a - * {@code .npmrc} may also arrive as a comma separated string. + * {@code .npmrc} may also arrive as a comma separated string. Only that + * string is split, as the values of an array are complete on their own and + * may contain a comma themselves, such as the brace expansion + * {@code @acme/{ui,core}}. * * @param toolCommand * the npm or pnpm command to run @@ -709,13 +712,11 @@ List getConfiguredSettingValues(List toolCommand, if (value == null || value.isNull()) { continue; } - List items = value.isArray() - ? value.valueStream().toList() - : List.of(value); - List values = items.stream() - .flatMap(item -> Stream.of(item.asString().split(","))) - .map(String::trim).filter(entry -> !entry.isEmpty()) - .toList(); + Stream entries = value.isArray() + ? value.valueStream().map(JsonNode::asString) + : Stream.of(value.asString().split(",")); + List values = entries.map(String::trim) + .filter(entry -> !entry.isEmpty()).toList(); if (!values.isEmpty()) { return values; } diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java index 4658c36b98d..d85a0a9f955 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskRunNpmInstall.java @@ -481,12 +481,16 @@ static String getToolName(Options options) { * installing frontend package versions that are too new, or nothing if the * age should not be restricted from here. *

    - * A value configured through Vaadin is always used as is, {@code 0} - * disabling the check. When nothing is configured, the package manager is - * asked what it resolves for its own minimum release age setting; if it - * already has one, no argument is passed so that the package manager - * applies its own configuration. Only when neither is configured does + * A value configured through Vaadin is always used as is, {@code 0} adding + * no restriction. When nothing is configured, the package manager is asked + * what it resolves for its own minimum release age setting; if it already + * has one, no argument is passed so that the package manager applies its + * own configuration. Only when neither is configured does * {@link #DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS} apply. + *

    + * An age the package manager resolves for itself is reported as applying + * even when Vaadin is configured with {@code 0}, as a command line argument + * is the only thing Vaadin leaves out in that case. * * @param options * current build options @@ -504,12 +508,19 @@ static MinimumFrontendPackageAge resolveMinimumFrontendPackageAge( Options options, FrontendTools tools, List toolCommand, Logger logger) { Integer configuredDays = options.getMinimumFrontendPackageAgeDays(); - if (configuredDays != null && configuredDays == 0) { - return new MinimumFrontendPackageAge(false, Optional.empty()); - } boolean npmSupportsMinReleaseAge = !options.isEnableBun() && !options.isEnablePnpm() && tools.npmSupportsMinReleaseAge(toolCommand); + if (configuredDays != null && configuredDays == 0) { + // Vaadin adds no restriction of its own, but an age the package + // manager is configured with still applies to the install + return new MinimumFrontendPackageAge( + getPackageManagerConfiguredMinimumReleaseAge(options, tools, + toolCommand, npmSupportsMinReleaseAge) + .filter(TaskRunNpmInstall::blocksSomeVersion) + .isPresent(), + Optional.empty()); + } int days; if (configuredDays != null) { days = configuredDays; diff --git a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTest.java b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTest.java index 58168d66c34..8cd60eba0f0 100644 --- a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTest.java +++ b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/FrontendToolsTest.java @@ -1025,6 +1025,25 @@ void getConfiguredSettingValues_listAndCommaSeparatedValue_areRead() } } + @Test + void getConfiguredSettingValues_braceExpansionInAList_isKeptTogether() + throws CommandExecutionException { + try (MockedStatic frontendUtils = Mockito + .mockStatic(FrontendUtils.class)) { + // the values of a list are complete on their own, and the comma + // of a brace expansion does not separate two patterns + frontendUtils + .when(() -> FrontendUtils.executeCommand(Mockito.anyList(), + Mockito.any())) + .thenReturn( + "{\"min-release-age-exclude\": [\"@acme/{ui,core}\"]}"); + + assertEquals(List.of("@acme/{ui,core}"), + tools.getConfiguredSettingValues(List.of("npm"), + new File(baseDir), "min-release-age-exclude")); + } + } + @Test void getConfiguredSettingValues_keyWithoutValue_isEmpty() throws CommandExecutionException { diff --git a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java index 295725b3a9a..ff76eeda544 100644 --- a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java +++ b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java @@ -889,6 +889,30 @@ void resolveMinimumFrontendPackageAge_bun_configurationIsNotRead() { Mockito.anyList(), Mockito.any(), Mockito.any(String[].class)); } + @Test + void resolveMinimumFrontendPackageAge_zeroConfiguredWithNpmrcValue_ageStillApplies() { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + // npm keeps applying the 7 days of its own configuration, as Vaadin + // only leaves out the command line argument + Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), + Mockito.eq(npmFolder), Mockito.eq("min-release-age"))) + .thenReturn(Optional.of("7")); + Options options = new MockOptions(npmFolder) + .withMinimumFrontendPackageAgeDays(0); + + TaskRunNpmInstall.MinimumFrontendPackageAge minimumAge = TaskRunNpmInstall + .resolveMinimumFrontendPackageAge(options, tools, + List.of("npm"), + LoggerFactory.getLogger(TaskRunNpmInstallTest.class)); + + assertTrue(minimumAge.applies()); + assertFalse(minimumAge.argument().isPresent()); + // so the packages Vaadin publishes are excluded from it + assertEquals(List.of("--min-release-age-exclude=@vaadin/*"), + resolveMinimumFrontendPackageAgeExcludeArguments(options, tools, + new MockLogger())); + } + @Test void resolveMinimumFrontendPackageAge_npmrcValueOfZero_noAgeApplies() { FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); From 7fbdd2f7a3007ed5dbdefa4126b128aab2216670 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 07:09:52 +0000 Subject: [PATCH 9/9] test: pin that nothing applies when the age check is turned off The zero case only checked the argument, so a regression could have turned the exclusions and the warning back on for a project that opted out. Also lets the new case resolve the exclusions from the age it resolved, instead of from a hardcoded flag, and fixes the braces of a javadoc example. --- .../flow/server/frontend/FrontendTools.java | 2 +- .../frontend/TaskRunNpmInstallTest.java | 33 +++++++++++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java index 0b00c162974..8fa0f58430e 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/FrontendTools.java @@ -691,7 +691,7 @@ Optional getConfiguredSetting(List toolCommand, * {@code .npmrc} may also arrive as a comma separated string. Only that * string is split, as the values of an array are complete on their own and * may contain a comma themselves, such as the brace expansion - * {@code @acme/{ui,core}}. + * {@code @acme/{ui,core}}. * * @param toolCommand * the npm or pnpm command to run diff --git a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java index ff76eeda544..005cbf8f46a 100644 --- a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java +++ b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java @@ -795,12 +795,31 @@ void resolveMinimumFrontendPackageAge_nothingConfigured_usesDefault() { } @Test - void resolveMinimumFrontendPackageAge_zeroConfigured_noArgument() { + void resolveMinimumFrontendPackageAge_zeroConfigured_noArgumentAndNoAge() { FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + Options options = new MockOptions(npmFolder) + .withMinimumFrontendPackageAgeDays(0); - assertFalse(resolveMinimumFrontendPackageAgeArgument( - new MockOptions(npmFolder).withMinimumFrontendPackageAgeDays(0), - tools).isPresent()); + // nothing blocks a version, so nothing has to be excluded either + assertMinimumFrontendPackageAge(options, tools, false, null); + + // an age npm is configured with is a value that blocks nothing too + Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), + Mockito.eq(npmFolder), Mockito.eq("min-release-age"))) + .thenReturn(Optional.of("0")); + + assertMinimumFrontendPackageAge(options, tools, false, null); + } + + private void assertMinimumFrontendPackageAge(Options options, + FrontendTools tools, boolean applies, String argument) { + TaskRunNpmInstall.MinimumFrontendPackageAge minimumAge = TaskRunNpmInstall + .resolveMinimumFrontendPackageAge(options, tools, + List.of(TaskRunNpmInstall.getToolName(options)), + LoggerFactory.getLogger(TaskRunNpmInstallTest.class)); + + assertEquals(applies, minimumAge.applies()); + assertEquals(Optional.ofNullable(argument), minimumAge.argument()); } @Test @@ -909,8 +928,10 @@ void resolveMinimumFrontendPackageAge_zeroConfiguredWithNpmrcValue_ageStillAppli assertFalse(minimumAge.argument().isPresent()); // so the packages Vaadin publishes are excluded from it assertEquals(List.of("--min-release-age-exclude=@vaadin/*"), - resolveMinimumFrontendPackageAgeExcludeArguments(options, tools, - new MockLogger())); + TaskRunNpmInstall + .resolveMinimumFrontendPackageAgeExcludeArguments( + options, tools, List.of("npm"), + minimumAge.applies(), new MockLogger())); } @Test