From a8abe7512e7ef00aab93baa5f2760338fc27340f Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Tue, 11 Aug 2026 14:28:43 +0300 Subject: [PATCH 1/4] fix: do not override the package manager minimum package age The minimumFrontendPackageAgeDays parameter defaulted to 1 and was always passed as a command line argument, which takes precedence over every configuration source of npm, pnpm and bun. A project that configures min-release-age in .npmrc got the Vaadin default instead, so `mvn vaadin:build-frontend` and a manually run `npm install` disagreed on which package versions are allowed. The parameter is now unset by default. When nothing is configured on the Vaadin side, the package manager is asked what it resolves for its own minimum release age setting (`config get` for npm and pnpm, bunfig.toml for bun) and no argument is passed when it already has one. The one day default applies only when neither is configured. --- .../flow/server/frontend/FrontendTools.java | 50 ++++++ .../vaadin/flow/server/frontend/Options.java | 46 ++++-- .../server/frontend/TaskRunNpmInstall.java | 154 ++++++++++++++++-- .../frontend/TaskRunNpmInstallTest.java | 129 +++++++++------ .../flow/plugin/maven/BuildDevBundleMojo.java | 17 +- .../com/vaadin/gradle/GradlePluginAdapter.kt | 6 +- .../gradle/VaadinFlowPluginExtension.kt | 12 +- .../flow/plugin/maven/BuildFrontendMojo.java | 17 +- .../flow/plugin/base/PluginAdapterBuild.java | 16 +- .../vaadin/flow/server/InitParameters.java | 10 +- .../devserver/startup/DevModeInitializer.java | 9 +- .../startup/DevModeInitializerTest.java | 11 +- 12 files changed, 370 insertions(+), 107 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 d21e7a96f15..b942ce89fe4 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 @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -634,6 +635,55 @@ Map getConfiguredRegistries(File workingDirectory) { return registries; } + /** + * Reads the value the given npm or pnpm command resolves for a + * configuration key by running {@code config get }. + *

+ * The value is read from the tool itself, so it accounts for every + * configuration source and precedence rule the tool applies (command line, + * environment variables, project/user/global {@code .npmrc} and, for pnpm, + * {@code pnpm-workspace.yaml}). + * + * @param toolCommand + * the npm or pnpm command to run + * @param key + * the configuration key to read + * @param workingDirectory + * the directory the configuration is resolved from, so that a + * project {@code .npmrc} is taken into account + * @return the configured value, or an empty optional if the key is not + * configured, is unknown to the tool, or the configuration cannot + * be read + */ + Optional getConfiguredSetting(List toolCommand, String key, + File workingDirectory) { + List command = new ArrayList<>(toolCommand); + command.add("config"); + command.add("get"); + command.add(key); + try { + String output = FrontendUtils.executeCommand(command, + builder -> builder.directory(workingDirectory)); + // Both tools print the value on its own line. npm prints "null" + // for a key it knows but that is not configured and "undefined" + // for an unknown key (which is what an npm older than + // MIN_NPM_VERSION_FOR_RELEASE_AGE reports for min-release-age); + // pnpm prints "undefined" when the key is not configured. + String value = output.lines().map(String::trim) + .filter(line -> !line.isEmpty()) + .reduce((first, last) -> last).orElse(""); + if (value.isEmpty() || "null".equals(value) + || "undefined".equals(value)) { + return Optional.empty(); + } + return Optional.of(value); + } catch (CommandExecutionException | RuntimeException e) { + getLogger().debug("Could not read the '{}' setting using '{}'", key, + String.join(" ", command), e); + return Optional.empty(); + } + } + /** * Extracts the custom (non-default) registry URLs from the given * configuration, normalized to always end with a slash so they can be 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 c7665b8f31b..3681452cfce 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 @@ -24,6 +24,7 @@ import java.util.Optional; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.slf4j.LoggerFactory; import tools.jackson.databind.JsonNode; @@ -175,11 +176,12 @@ public class Options implements Serializable { /** * Minimum age, in days, that an npm/pnpm/bun frontend package version must - * have before it is allowed to be installed. Defaults to {@code 1} day as a - * mitigation against malicious packages published to the registry; set to - * {@code 0} to disable. + * have before it is allowed to be installed, or {@code null} when nothing + * has been configured on the Vaadin side. In the latter case the value is + * resolved from the package manager configuration, falling back to + * {@link TaskRunNpmInstall#DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS}. */ - private int minimumFrontendPackageAgeDays = 1; + private @Nullable Integer minimumFrontendPackageAgeDays; private ApplicationConfiguration applicationConfiguration; @@ -1179,21 +1181,34 @@ public Options withCommercialBanner(boolean enableCommercialBanner) { * to avoid pulling in brand-new versions that may have been compromised by * a supply-chain attack but not yet detected and removed from the registry. *

- * For npm this is translated to a {@code --before=} argument; for - * pnpm it becomes {@code --config.minimum-release-age=} (requires - * pnpm ≥ 10.16.0); for bun it becomes - * {@code --minimum-release-age=} (requires bun ≥ 1.3.0). + * For npm this is translated to a {@code --min-release-age=} + * argument, or {@code --before=} for npm older than 11.10.0; for pnpm + * it becomes {@code --config.minimum-release-age=} (requires pnpm + * ≥ 10.16.0); for bun it becomes {@code --minimum-release-age=} + * (requires bun ≥ 1.3.0). Since these are command line arguments, they + * take precedence over anything the package manager reads from its own + * configuration. + *

+ * When set to {@code null}, no such argument is passed if the package + * manager already resolves a minimum release age from its own configuration + * ({@code .npmrc}, {@code pnpm-workspace.yaml} or {@code bunfig.toml}), so + * that a manually run {@code npm install} behaves the same way. Only if + * nothing is configured there, + * {@link TaskRunNpmInstall#DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS} is + * used. * * @param minimumFrontendPackageAgeDays - * minimum allowed age in days, or {@code 0} to disable the check + * minimum allowed age in days, {@code 0} to disable the check, + * or {@code null} to use the package manager configuration * @return this builder * @throws IllegalArgumentException * if {@code minimumFrontendPackageAgeDays} is negative * @since 25.1.6 */ public Options withMinimumFrontendPackageAgeDays( - int minimumFrontendPackageAgeDays) { - if (minimumFrontendPackageAgeDays < 0) { + @Nullable Integer minimumFrontendPackageAgeDays) { + if (minimumFrontendPackageAgeDays != null + && minimumFrontendPackageAgeDays < 0) { throw new IllegalArgumentException( "minimumFrontendPackageAgeDays must be >= 0"); } @@ -1204,12 +1219,15 @@ public Options withMinimumFrontendPackageAgeDays( /** * Gets the minimum age (in days) a frontend package version must have * before npm, pnpm or bun is allowed to install it. {@code 0} means the - * check is disabled. + * check is disabled and {@code null} means that nothing has been configured + * on the Vaadin side, in which case the package manager configuration + * decides. See {@link #withMinimumFrontendPackageAgeDays(Integer)}. * - * @return the minimum allowed age in days + * @return the minimum allowed age in days, or {@code null} if not + * configured * @since 25.1.6 */ - public int getMinimumFrontendPackageAgeDays() { + public @Nullable Integer getMinimumFrontendPackageAgeDays() { return minimumFrontendPackageAgeDays; } 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 e199718960b..87d73de7c19 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 @@ -37,14 +37,17 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import tools.jackson.databind.JsonNode; import com.vaadin.flow.internal.FileIOUtils; import com.vaadin.flow.internal.FrontendUtils; import com.vaadin.flow.internal.Pair; import com.vaadin.flow.server.Constants; +import com.vaadin.flow.server.InitParameters; import com.vaadin.flow.shared.util.SharedUtil; import static com.vaadin.flow.internal.FrontendUtils.commandToString; @@ -62,6 +65,17 @@ */ public class TaskRunNpmInstall implements FallibleCommand { + /** + * The minimum age, in days, a frontend package version must have before it + * is installed, used when neither Vaadin nor the package manager itself has + * been configured with a value. Mitigates supply-chain attacks where a + * compromised version is briefly available on the registry. + */ + static final int DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS = 1; + + private static final Pattern BUNFIG_MINIMUM_RELEASE_AGE = Pattern + .compile("minimumReleaseAge\\s*=\\s*(\\S+)"); + private static final String MODULES_YAML = ".modules.yaml"; private static final String NPM_VALIDATION_FAIL_MESSAGE = "%n%n======================================================================================================" @@ -292,12 +306,17 @@ private void runNpmInstall() throws ExecutionFailedException { } } - boolean npmSupportsMinReleaseAge = options - .getMinimumFrontendPackageAgeDays() > 0 - && !options.isEnableBun() && !options.isEnablePnpm() - && tools.npmSupportsMinReleaseAge(npmExecutable); - getMinimumFrontendPackageAgeArgument(options, npmSupportsMinReleaseAge) - .ifPresent(npmInstallCommand::add); + Integer configuredPackageAgeDays = options + .getMinimumFrontendPackageAgeDays(); + if (configuredPackageAgeDays == null || configuredPackageAgeDays > 0) { + boolean npmSupportsMinReleaseAge = !options.isEnableBun() + && !options.isEnablePnpm() + && tools.npmSupportsMinReleaseAge(npmExecutable); + int packageAgeDays = resolveMinimumFrontendPackageAgeDays(options, + tools, npmExecutable, npmSupportsMinReleaseAge, logger); + getMinimumFrontendPackageAgeArgument(options, packageAgeDays, + npmSupportsMinReleaseAge).ifPresent(npmInstallCommand::add); + } postinstallCommand.add("run"); postinstallCommand.add("postinstall"); @@ -436,14 +455,128 @@ static String getToolName(Options options) { } } + /** + * Resolves how old a frontend package version must be before the active + * package manager is allowed to install it. + *

+ * A value configured through Vaadin is always used as is. When nothing is + * configured, the package manager is asked what it resolves for its own + * minimum release age setting; if it already has one, {@code 0} is returned + * so that no argument is passed and the package manager applies its own + * configuration. Only when neither is configured does + * {@link #DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS} apply. + * + * @param options + * current build options + * @param tools + * the frontend tools used to read the package manager + * configuration + * @param toolCommand + * the npm, pnpm or bun command used for the install + * @param npmSupportsMinReleaseAge + * {@code true} if the active npm understands + * {@code --min-release-age} (npm 11.10+) + * @param logger + * the logger to report the resolved source of the value to + * @return the minimum age in days, or {@code 0} to not restrict the age + */ + static int resolveMinimumFrontendPackageAgeDays(Options options, + FrontendTools tools, List toolCommand, + boolean npmSupportsMinReleaseAge, Logger logger) { + Integer configuredDays = options.getMinimumFrontendPackageAgeDays(); + if (configuredDays != null) { + return configuredDays; + } + Optional packageManagerValue = getPackageManagerMinimumReleaseAge( + options, tools, toolCommand, npmSupportsMinReleaseAge); + if (packageManagerValue.isPresent()) { + logger.info( + "Keeping the minimum frontend package age configured for {} " + + "({}) instead of applying the Vaadin default. Set the " + + "'{}' parameter to override it.", + getToolName(options), packageManagerValue.get(), + InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS); + return 0; + } + return DEFAULT_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 + * line argument. + */ + private static Optional getPackageManagerMinimumReleaseAge( + Options options, FrontendTools tools, List toolCommand, + boolean npmSupportsMinReleaseAge) { + File npmFolder = options.getNpmFolder(); + if (options.isEnableBun()) { + // bun has no command for printing its resolved configuration + return getBunfigMinimumReleaseAge(npmFolder); + } + if (options.isEnablePnpm()) { + // pnpm reads this from .npmrc and pnpm-workspace.yaml alike + return tools.getConfiguredSetting(toolCommand, + "minimum-release-age", npmFolder); + } + if (npmSupportsMinReleaseAge) { + return tools.getConfiguredSetting(toolCommand, "min-release-age", + npmFolder); + } + // Older npm has no min-release-age setting, but the --before argument + // used as a fallback does have a configuration counterpart + return tools.getConfiguredSetting(toolCommand, "before", npmFolder); + } + + /** + * Reads the {@code minimumReleaseAge} setting from the project and user + * {@code bunfig.toml} files. The setting is only valid in the + * {@code [install]} section, which is the only section it is looked for in. + */ + private static Optional getBunfigMinimumReleaseAge(File npmFolder) { + return Stream + .of(new File(npmFolder, "bunfig.toml"), + new File(FileIOUtils.getUserDirectory(), + ".bunfig.toml")) + .filter(File::canRead) + .map(TaskRunNpmInstall::getBunfigMinimumReleaseAgeFromFile) + .flatMap(Optional::stream).findFirst(); + } + + private static Optional getBunfigMinimumReleaseAgeFromFile( + File bunfig) { + try { + boolean inInstallSection = false; + for (String line : Files.readAllLines(bunfig.toPath(), + StandardCharsets.UTF_8)) { + String trimmed = line.trim(); + if (trimmed.startsWith("[")) { + inInstallSection = "[install]".equals(trimmed); + } else if (inInstallSection) { + Matcher matcher = BUNFIG_MINIMUM_RELEASE_AGE + .matcher(trimmed); + if (matcher.lookingAt()) { + return Optional.of(matcher.group(1)); + } + } + } + } catch (IOException e) { + LoggerFactory.getLogger(TaskRunNpmInstall.class).debug( + "Could not read the minimumReleaseAge setting from {}", + bunfig, e); + } + return Optional.empty(); + } + /** * Builds the install argument that prevents npm, pnpm or bun from - * installing frontend package versions newer than - * {@link Options#getMinimumFrontendPackageAgeDays()} days. Returns an empty - * optional when the check is disabled. + * installing frontend package versions newer than the given number of days. + * Returns an empty optional when the age is not restricted. * * @param options * current build options + * @param days + * the minimum age in days, or {@code 0} to not restrict the age * @param npmSupportsMinReleaseAge * {@code true} if the active npm understands * {@code --min-release-age} (npm 11.10+); when {@code false} the @@ -451,8 +584,7 @@ static String getToolName(Options options) { * when bun or pnpm is enabled. */ static Optional getMinimumFrontendPackageAgeArgument( - Options options, boolean npmSupportsMinReleaseAge) { - int days = options.getMinimumFrontendPackageAgeDays(); + Options options, int days, boolean npmSupportsMinReleaseAge) { if (days <= 0) { return Optional.empty(); } 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 bc808061a7b..28c04c3ebaa 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 @@ -766,70 +766,37 @@ private void assumeNPMIsInUse() { assumeTrue(getClass().equals(TaskRunNpmInstallTest.class)); } - @Test - void minimumFrontendPackageAge_defaultIsOneDay_addsArgument() { - // Default is 1 day → 1440 minutes for pnpm, 86400 seconds for bun, - // --min-release-age=1 for npm 11.10+, and --before= for - // older npm - assertEquals("--min-release-age=1", - TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument( - new MockOptions(npmFolder), true) - .orElseThrow()); - assertTrue(TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument( - new MockOptions(npmFolder), false) - .orElseThrow().startsWith("--before=")); - assertEquals("--config.minimum-release-age=1440", - TaskRunNpmInstall.getMinimumFrontendPackageAgeArgument( - new MockOptions(npmFolder).withEnablePnpm(true), false) - .orElseThrow()); - assertEquals("--minimum-release-age=86400", - TaskRunNpmInstall.getMinimumFrontendPackageAgeArgument( - new MockOptions(npmFolder).withEnableBun(true), false) - .orElseThrow()); - } - @Test void minimumFrontendPackageAge_zeroDisablesCheck_returnsEmpty() { - Options npmOptions = new MockOptions(npmFolder) - .withMinimumFrontendPackageAgeDays(0); + Options npmOptions = new MockOptions(npmFolder); assertFalse(TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument(npmOptions, true) + .getMinimumFrontendPackageAgeArgument(npmOptions, 0, true) .isPresent()); assertFalse(TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument(npmOptions, false) + .getMinimumFrontendPackageAgeArgument(npmOptions, 0, false) .isPresent()); - assertFalse(TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument( - new MockOptions(npmFolder).withEnablePnpm(true) - .withMinimumFrontendPackageAgeDays(0), - false) + assertFalse(TaskRunNpmInstall.getMinimumFrontendPackageAgeArgument( + new MockOptions(npmFolder).withEnablePnpm(true), 0, false) .isPresent()); - assertFalse(TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument( - new MockOptions(npmFolder).withEnableBun(true) - .withMinimumFrontendPackageAgeDays(0), - false) + assertFalse(TaskRunNpmInstall.getMinimumFrontendPackageAgeArgument( + new MockOptions(npmFolder).withEnableBun(true), 0, false) .isPresent()); } @Test void minimumFrontendPackageAge_npmNewEnough_addsMinReleaseAgeArgument() { - Options npmOptions = new MockOptions(npmFolder) - .withMinimumFrontendPackageAgeDays(2); + Options npmOptions = new MockOptions(npmFolder); Optional arg = TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument(npmOptions, true); + .getMinimumFrontendPackageAgeArgument(npmOptions, 2, true); // npm 11.10+: --min-release-age takes a value in days assertEquals("--min-release-age=2", arg.orElseThrow()); } @Test void minimumFrontendPackageAge_npmTooOld_fallsBackToBeforeArgument() { - Options npmOptions = new MockOptions(npmFolder) - .withMinimumFrontendPackageAgeDays(2); + Options npmOptions = new MockOptions(npmFolder); Optional arg = TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument(npmOptions, false); + .getMinimumFrontendPackageAgeArgument(npmOptions, 2, false); assertTrue(arg.isPresent()); assertTrue(arg.get().startsWith("--before="), "Older npm should fall back to --before, was: " + arg.get()); @@ -837,23 +804,79 @@ void minimumFrontendPackageAge_npmTooOld_fallsBackToBeforeArgument() { @Test void minimumFrontendPackageAge_pnpm_addsMinimumReleaseAgeArgument() { - Options pnpmOptions = new MockOptions(npmFolder).withEnablePnpm(true) - .withMinimumFrontendPackageAgeDays(2); + Options pnpmOptions = new MockOptions(npmFolder).withEnablePnpm(true); Optional arg = TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument(pnpmOptions, false); + .getMinimumFrontendPackageAgeArgument(pnpmOptions, 2, false); // 2 days = 2880 minutes; pnpm setting form assertEquals("--config.minimum-release-age=2880", arg.orElseThrow()); } @Test void minimumFrontendPackageAge_bun_addsMinimumReleaseAgeInSeconds() { - Options bunOptions = new MockOptions(npmFolder).withEnableBun(true) - .withMinimumFrontendPackageAgeDays(2); + Options bunOptions = new MockOptions(npmFolder).withEnableBun(true); // 2 days = 172800 seconds - assertEquals("--minimum-release-age=172800", - TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument(bunOptions, false) - .orElseThrow()); + assertEquals("--minimum-release-age=172800", TaskRunNpmInstall + .getMinimumFrontendPackageAgeArgument(bunOptions, 2, false) + .orElseThrow()); + } + + @Test + void resolveMinimumFrontendPackageAge_nothingConfigured_usesDefault() { + FrontendTools tools = Mockito.mock(FrontendTools.class); + Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), + Mockito.anyString(), Mockito.any())) + .thenReturn(Optional.empty()); + + assertEquals( + TaskRunNpmInstall.DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS, + resolveMinimumFrontendPackageAge(new MockOptions(npmFolder), + tools)); + } + + @Test + void resolveMinimumFrontendPackageAge_npmrcValue_doesNotOverrideIt() { + FrontendTools tools = Mockito.mock(FrontendTools.class); + Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), + Mockito.eq("min-release-age"), Mockito.eq(npmFolder))) + .thenReturn(Optional.of("7")); + + // 0 means no argument is passed, so npm applies its own configuration + assertEquals(0, resolveMinimumFrontendPackageAge( + new MockOptions(npmFolder), tools)); + } + + @Test + void resolveMinimumFrontendPackageAge_configuredInVaadin_overridesNpmrcValue() { + FrontendTools tools = Mockito.mock(FrontendTools.class); + Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), + Mockito.eq("min-release-age"), Mockito.eq(npmFolder))) + .thenReturn(Optional.of("7")); + + assertEquals(3, resolveMinimumFrontendPackageAge( + new MockOptions(npmFolder).withMinimumFrontendPackageAgeDays(3), + tools)); + } + + @Test + void resolveMinimumFrontendPackageAge_bunfigValue_doesNotOverrideIt() + throws IOException { + Files.writeString(new File(npmFolder, "bunfig.toml").toPath(), """ + [install] + # minimumReleaseAge = 1 + minimumReleaseAge = 604800 + """); + + assertEquals(0, + resolveMinimumFrontendPackageAge( + new MockOptions(npmFolder).withEnableBun(true), + Mockito.mock(FrontendTools.class))); + } + + private int resolveMinimumFrontendPackageAge(Options options, + FrontendTools tools) { + return TaskRunNpmInstall.resolveMinimumFrontendPackageAgeDays(options, + tools, List.of("npm"), true, + LoggerFactory.getLogger(TaskRunNpmInstallTest.class)); } } 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 6ad34846969..0b869c41113 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 @@ -214,13 +214,18 @@ public class BuildDevBundleMojo extends AbstractMojo /** * Minimum age (in days) a frontend (npm) package version must have before * npm, pnpm or bun is allowed to install it. Mitigates supply-chain attacks - * where a compromised version is briefly available on the registry. - * Defaults to {@code 1} day; set to {@code 0} to disable. Requires pnpm - * ≥ 10.16.0 or bun ≥ 1.3.0 when those tools are used. + * where a compromised version is briefly available on the registry. Set to + * {@code 0} to disable. Requires pnpm ≥ 10.16.0 or bun ≥ 1.3.0 when + * those tools are used. + *

+ * When not set, the value configured for the package manager itself + * ({@code .npmrc}, {@code pnpm-workspace.yaml} or {@code bunfig.toml}) is + * used, so that a manually run {@code npm install} behaves the same way. + * Only when there is no such value does the check default to {@code 1} day. */ @Parameter(property = "vaadin." - + InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS, defaultValue = "1") - private int minimumFrontendPackageAgeDays; + + InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS) + private Integer minimumFrontendPackageAgeDays; /** * The folder where the META-INF/resources files are copied. Used for @@ -591,7 +596,7 @@ public boolean isFrontendIgnoreVersionChecks() { } @Override - public int minimumFrontendPackageAgeDays() { + public Integer minimumFrontendPackageAgeDays() { return minimumFrontendPackageAgeDays; } diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt index ed674b1b02f..3499e89ed94 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt @@ -373,7 +373,9 @@ internal class GradlePluginAdapter private constructor( return config.commercialWithBanner.get() } - override fun minimumFrontendPackageAgeDays(): Int = - config.minimumFrontendPackageAgeDays.get() + // Null when not configured, so that the value configured for the package + // manager itself is used instead of being overridden + override fun minimumFrontendPackageAgeDays(): Int? = + config.minimumFrontendPackageAgeDays.orNull } diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt index 142c0ba0aae..416a6164a33 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt @@ -360,8 +360,14 @@ public abstract class VaadinFlowPluginExtension @Inject constructor(private val * Minimum age (in days) a frontend (npm) package version must have before * npm, pnpm or bun is allowed to install it. Mitigates supply-chain * attacks where a compromised version is briefly available on the - * registry. Defaults to {@code 1} day; set to {@code 0} to disable. - * Requires pnpm >= 10.16.0 or bun >= 1.3.0 when those tools are used. + * registry. Set to {@code 0} to disable. Requires pnpm >= 10.16.0 or bun + * >= 1.3.0 when those tools are used. + * + * When not set, the value configured for the package manager itself + * ({@code .npmrc}, {@code pnpm-workspace.yaml} or {@code bunfig.toml}) is + * used, so that a manually run {@code npm install} behaves the same way. + * Only when there is no such value does the check default to {@code 1} + * day. */ public abstract val minimumFrontendPackageAgeDays: Property @@ -677,7 +683,7 @@ public class PluginEffectiveConfiguration( project.getStringProperty( "vaadin.${InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS}" ).map(String::toInt) - .orElse(extension.minimumFrontendPackageAgeDays.convention(1)) + .orElse(extension.minimumFrontendPackageAgeDays) public val npmExcludeWebComponents: Provider = extension .npmExcludeWebComponents.convention(false) 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 2e1b2fdf106..a3453695199 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 @@ -161,13 +161,18 @@ public class BuildFrontendMojo extends FlowModeAbstractMojo /** * Minimum age (in days) a frontend (npm) package version must have before * npm, pnpm or bun is allowed to install it. Mitigates supply-chain attacks - * where a compromised version is briefly available on the registry. - * Defaults to {@code 1} day; set to {@code 0} to disable. Requires pnpm - * ≥ 10.16.0 or bun ≥ 1.3.0 when those tools are used. + * where a compromised version is briefly available on the registry. Set to + * {@code 0} to disable. Requires pnpm ≥ 10.16.0 or bun ≥ 1.3.0 when + * those tools are used. + *

+ * When not set, the value configured for the package manager itself + * ({@code .npmrc}, {@code pnpm-workspace.yaml} or {@code bunfig.toml}) is + * used, so that a manually run {@code npm install} behaves the same way. + * Only when there is no such value does the check default to {@code 1} day. */ @Parameter(property = "vaadin." - + InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS, defaultValue = "1") - private int minimumFrontendPackageAgeDays; + + InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS) + private Integer minimumFrontendPackageAgeDays; @Override protected void executeInternal() @@ -328,7 +333,7 @@ public File resourcesOutputDirectory() { } @Override - public int minimumFrontendPackageAgeDays() { + public Integer minimumFrontendPackageAgeDays() { return minimumFrontendPackageAgeDays; } diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBuild.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBuild.java index 0767df82915..2bf3647c97b 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBuild.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBuild.java @@ -129,13 +129,17 @@ boolean checkRuntimeDependency(String groupId, String artifactId, /** * Minimum age (in days) a frontend package version must have before npm, - * pnpm or bun is allowed to install it. Defaults to {@code 1} day as a - * mitigation against malicious packages briefly published to the registry; - * set to {@code 0} to disable. + * pnpm or bun is allowed to install it, as a mitigation against malicious + * packages briefly published to the registry. {@code 0} disables the check. + *

+ * When {@code null}, the value configured for the package manager itself + * ({@code .npmrc}, {@code pnpm-workspace.yaml} or {@code bunfig.toml}) is + * used, defaulting to one day if there is none. * - * @return the minimum allowed age in days, or {@code 0} when disabled + * @return the minimum allowed age in days, {@code 0} when disabled, or + * {@code null} when not configured */ - default int minimumFrontendPackageAgeDays() { - return 1; + default Integer minimumFrontendPackageAgeDays() { + return null; } } 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 8f1c5fe2395..8db53565299 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 @@ -409,9 +409,13 @@ public class InitParameters implements Serializable { /** * Configuration name for the minimum age (in days) a frontend (npm) package - * version must have before npm, pnpm or bun is allowed to install it. - * Defaults to {@code 1} day; set to {@code 0} to disable. - * + * version must have before npm, pnpm or bun is allowed to install it. Set + * to {@code 0} to disable. + *

+ * When not set, the value configured for the package manager itself + * ({@code .npmrc}, {@code pnpm-workspace.yaml} or {@code bunfig.toml}) is + * used, defaulting to {@code 1} day if there is none. + * * @since 25.1.6 */ public static final String MINIMUM_FRONTEND_PACKAGE_AGE_DAYS = "npm.minimumFrontendPackageAgeDays"; diff --git a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/startup/DevModeInitializer.java b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/startup/DevModeInitializer.java index b0c73e2f0ab..192b0683dad 100644 --- a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/startup/DevModeInitializer.java +++ b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/startup/DevModeInitializer.java @@ -293,8 +293,13 @@ public static DevModeHandler initDevModeHandler(Set> classes, boolean npmExcludeWebComponents = config .getBooleanProperty(NPM_EXCLUDE_WEB_COMPONENTS, false); - int minimumFrontendPackageAgeDays = Integer.parseInt(config - .getStringProperty(MINIMUM_FRONTEND_PACKAGE_AGE_DAYS, "1")); + // Left as null when not configured, so that the value configured for + // the package manager itself is used instead of being overridden + String minimumFrontendPackageAge = config + .getStringProperty(MINIMUM_FRONTEND_PACKAGE_AGE_DAYS, null); + Integer minimumFrontendPackageAgeDays = minimumFrontendPackageAge == null + ? null + : Integer.valueOf(minimumFrontendPackageAge); options.enablePackagesUpdate(true) .useByteCodeScanner(useByteCodeScanner) diff --git a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/startup/DevModeInitializerTest.java b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/startup/DevModeInitializerTest.java index 314c09ec570..87d1808ea0b 100644 --- a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/startup/DevModeInitializerTest.java +++ b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/startup/DevModeInitializerTest.java @@ -260,13 +260,22 @@ void should_Run_Updaters_when_NoNodeConfFiles() throws Exception { void minimumFrontendPackageAgeDays_readFromConfig_passedToOptions() throws Exception { Mockito.when(appConfig.getStringProperty( - InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS, "1")) + InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS, null)) .thenReturn("7"); assertEquals(7, captureNodeTasksOptions().getMinimumFrontendPackageAgeDays()); } + @Test + void minimumFrontendPackageAgeDays_notInConfig_leftUnsetInOptions() + throws Exception { + // Null lets the package manager configuration decide instead of + // overriding it with a command line argument + assertNull( + captureNodeTasksOptions().getMinimumFrontendPackageAgeDays()); + } + private Options captureNodeTasksOptions() throws Exception { List captured = new ArrayList<>(); try (MockedConstruction ignored = Mockito From 973f85a59d1cfd8645736c0671ea468c0e467ff2 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Tue, 11 Aug 2026 15:32:14 +0300 Subject: [PATCH 2/4] docs: fix javadoc reference to the minimum package age setter The parameter type changed from int to Integer, which the javadoc build flags as a reference that cannot be resolved. --- .../java/com/vaadin/flow/server/frontend/FrontendTools.java | 6 +++--- 1 file changed, 3 insertions(+), 3 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 b942ce89fe4..ccac0bfd80f 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 @@ -727,9 +727,9 @@ public FrontendVersion getNpmVersion() throws UnknownVersionException { * the {@code --min-release-age} install flag (see * {@link #MIN_NPM_VERSION_FOR_RELEASE_AGE}). Used when building the * {@code npm install} command for the minimum-package-age check (see - * {@link Options#withMinimumFrontendPackageAgeDays(int)}) to decide between - * {@code --min-release-age} and the {@code --before=} fallback - * supported by older npm versions. + * {@link Options#withMinimumFrontendPackageAgeDays(Integer)}) to decide + * between {@code --min-release-age} and the {@code --before=} + * fallback supported by older npm versions. * * @param npmCommand * the npm command to invoke for {@code --version} From 3ea430ed6808d22c79df7746e057c71a8064f8d2 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Tue, 11 Aug 2026 15:57:27 +0300 Subject: [PATCH 3/4] refactor: read package manager settings through one config call Reading the minimum release age used `config get ` while the registry lookup already ran `config ls --json`, which resolves the same configuration and contains every key. Both now share getResolvedConfiguration, so there is a single way to ask npm or pnpm what it resolves for a directory, and no string parsing of the tool output is needed to tell a configured value from an unset one. --- .../flow/server/frontend/FrontendTools.java | 80 ++++++++++--------- 1 file changed, 41 insertions(+), 39 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 ccac0bfd80f..b58d00c913a 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 @@ -613,36 +613,21 @@ Set getCustomNpmRegistries(File workingDirectory) { * map if the configuration cannot be read */ Map getConfiguredRegistries(File workingDirectory) { - List command = new ArrayList<>(getNpmExecutable(false)); - command.add("config"); - command.add("ls"); - command.add("--json"); + JsonNode config = getResolvedConfiguration(getNpmExecutable(false), + workingDirectory); Map registries = new HashMap<>(); - try { - String output = FrontendUtils.executeCommand(command, - builder -> builder.directory(workingDirectory)); - JsonNode config = JacksonUtils.readTree(output); - for (String key : config.propertyNames()) { - if ((key.equals("registry") || key.endsWith(":registry")) - && config.get(key).isString()) { - registries.put(key, config.get(key).asString()); - } + for (String key : config.propertyNames()) { + if ((key.equals("registry") || key.endsWith(":registry")) + && config.get(key).isString()) { + registries.put(key, config.get(key).asString()); } - } catch (CommandExecutionException | RuntimeException e) { - getLogger().debug("Could not read the npm registry configuration; " - + "assuming the default registry.", e); } return registries; } /** * Reads the value the given npm or pnpm command resolves for a - * configuration key by running {@code config get }. - *

- * The value is read from the tool itself, so it accounts for every - * configuration source and precedence rule the tool applies (command line, - * environment variables, project/user/global {@code .npmrc} and, for pnpm, - * {@code pnpm-workspace.yaml}). + * configuration key. * * @param toolCommand * the npm or pnpm command to run @@ -657,30 +642,47 @@ Map getConfiguredRegistries(File workingDirectory) { */ Optional getConfiguredSetting(List toolCommand, String key, File workingDirectory) { + JsonNode value = getResolvedConfiguration(toolCommand, workingDirectory) + .get(key); + // npm lists every key it knows, using null for the ones that are not + // configured; pnpm lists only the configured ones + if (value == null || value.isNull()) { + return Optional.empty(); + } + return Optional.of(value.asString()); + } + + /** + * Reads the configuration the given npm or pnpm command resolves for a + * directory by running {@code config ls --json}. + *

+ * The configuration is read from the tool itself, so it accounts for every + * configuration source and precedence rule the tool applies (command line, + * environment variables, project/user/global/builtin {@code .npmrc} and, + * for pnpm, {@code pnpm-workspace.yaml}). + * + * @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 + * @return the resolved configuration, or an empty object if it cannot be + * read + */ + JsonNode getResolvedConfiguration(List toolCommand, + File workingDirectory) { List command = new ArrayList<>(toolCommand); command.add("config"); - command.add("get"); - command.add(key); + command.add("ls"); + command.add("--json"); try { String output = FrontendUtils.executeCommand(command, builder -> builder.directory(workingDirectory)); - // Both tools print the value on its own line. npm prints "null" - // for a key it knows but that is not configured and "undefined" - // for an unknown key (which is what an npm older than - // MIN_NPM_VERSION_FOR_RELEASE_AGE reports for min-release-age); - // pnpm prints "undefined" when the key is not configured. - String value = output.lines().map(String::trim) - .filter(line -> !line.isEmpty()) - .reduce((first, last) -> last).orElse(""); - if (value.isEmpty() || "null".equals(value) - || "undefined".equals(value)) { - return Optional.empty(); - } - return Optional.of(value); + return JacksonUtils.readTree(output); } catch (CommandExecutionException | RuntimeException e) { - getLogger().debug("Could not read the '{}' setting using '{}'", key, + getLogger().debug("Could not read the configuration using '{}'", String.join(" ", command), e); - return Optional.empty(); + return JacksonUtils.createObjectNode(); } } From 2a1e53c627f5f2ebff7091e76d3fa94ea1a82e1f Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Tue, 11 Aug 2026 16:11:07 +0300 Subject: [PATCH 4/4] refactor: decide the package age argument instead of a day count resolveMinimumFrontendPackageAgeDays returned 0 both for a check that is explicitly disabled and for one the package manager already handles itself, and the caller repeated the same check to know whether to ask npm for its version at all. Resolution now returns the install argument to add, or nothing, and the remaining day count method only formats a positive number of days for the package manager in use. --- .../server/frontend/TaskRunNpmInstall.java | 96 +++++++------- .../frontend/TaskRunNpmInstallTest.java | 121 +++++++++--------- 2 files changed, 105 insertions(+), 112 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 87d73de7c19..c78a08db2be 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 @@ -306,17 +306,8 @@ private void runNpmInstall() throws ExecutionFailedException { } } - Integer configuredPackageAgeDays = options - .getMinimumFrontendPackageAgeDays(); - if (configuredPackageAgeDays == null || configuredPackageAgeDays > 0) { - boolean npmSupportsMinReleaseAge = !options.isEnableBun() - && !options.isEnablePnpm() - && tools.npmSupportsMinReleaseAge(npmExecutable); - int packageAgeDays = resolveMinimumFrontendPackageAgeDays(options, - tools, npmExecutable, npmSupportsMinReleaseAge, logger); - getMinimumFrontendPackageAgeArgument(options, packageAgeDays, - npmSupportsMinReleaseAge).ifPresent(npmInstallCommand::add); - } + resolveMinimumFrontendPackageAgeArgument(options, tools, npmExecutable, + logger).ifPresent(npmInstallCommand::add); postinstallCommand.add("run"); postinstallCommand.add("postinstall"); @@ -456,14 +447,15 @@ static String getToolName(Options options) { } /** - * Resolves how old a frontend package version must be before the active - * package manager is allowed to install it. + * Resolves the install argument that stops the active package manager from + * 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. When nothing is - * configured, the package manager is asked what it resolves for its own - * minimum release age setting; if it already has one, {@code 0} is returned - * so that no argument is passed and 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} + * 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 * {@link #DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS} apply. * * @param options @@ -473,32 +465,40 @@ static String getToolName(Options options) { * configuration * @param toolCommand * the npm, pnpm or bun command used for the install - * @param npmSupportsMinReleaseAge - * {@code true} if the active npm understands - * {@code --min-release-age} (npm 11.10+) * @param logger * the logger to report the resolved source of the value to - * @return the minimum age in days, or {@code 0} to not restrict the age + * @return the install argument, or an empty optional if none should be + * passed */ - static int resolveMinimumFrontendPackageAgeDays(Options options, - FrontendTools tools, List toolCommand, - boolean npmSupportsMinReleaseAge, Logger logger) { + static Optional resolveMinimumFrontendPackageAgeArgument( + Options options, FrontendTools tools, List toolCommand, + Logger logger) { Integer configuredDays = options.getMinimumFrontendPackageAgeDays(); + if (configuredDays != null && configuredDays == 0) { + return Optional.empty(); + } + boolean npmSupportsMinReleaseAge = !options.isEnableBun() + && !options.isEnablePnpm() + && tools.npmSupportsMinReleaseAge(toolCommand); + int days; if (configuredDays != null) { - return configuredDays; - } - Optional packageManagerValue = getPackageManagerMinimumReleaseAge( - options, tools, toolCommand, npmSupportsMinReleaseAge); - if (packageManagerValue.isPresent()) { - logger.info( - "Keeping the minimum frontend package age configured for {} " - + "({}) instead of applying the Vaadin default. Set the " - + "'{}' parameter to override it.", - getToolName(options), packageManagerValue.get(), - InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS); - return 0; - } - return DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS; + days = configuredDays; + } else { + Optional packageManagerValue = getPackageManagerMinimumReleaseAge( + options, tools, toolCommand, npmSupportsMinReleaseAge); + if (packageManagerValue.isPresent()) { + logger.info( + "Keeping the minimum frontend package age configured for {} " + + "({}) instead of applying the Vaadin default. Set the " + + "'{}' parameter to override it.", + getToolName(options), packageManagerValue.get(), + InitParameters.MINIMUM_FRONTEND_PACKAGE_AGE_DAYS); + return Optional.empty(); + } + days = DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS; + } + return Optional.of(getMinimumFrontendPackageAgeArgument(options, days, + npmSupportsMinReleaseAge)); } /** @@ -571,42 +571,38 @@ private static Optional getBunfigMinimumReleaseAgeFromFile( /** * Builds the install argument that prevents npm, pnpm or bun from * installing frontend package versions newer than the given number of days. - * Returns an empty optional when the age is not restricted. * * @param options * current build options * @param days - * the minimum age in days, or {@code 0} to not restrict the age + * the minimum age in days, always positive * @param npmSupportsMinReleaseAge * {@code true} if the active npm understands * {@code --min-release-age} (npm 11.10+); when {@code false} the * legacy {@code --before=} flag is used instead. Ignored * when bun or pnpm is enabled. */ - static Optional getMinimumFrontendPackageAgeArgument( - Options options, int days, boolean npmSupportsMinReleaseAge) { - if (days <= 0) { - return Optional.empty(); - } + static String getMinimumFrontendPackageAgeArgument(Options options, + int days, boolean npmSupportsMinReleaseAge) { if (options.isEnableBun()) { // bun: --minimum-release-age takes a value in seconds long seconds = (long) days * 24 * 60 * 60; - return Optional.of("--minimum-release-age=" + seconds); + return "--minimum-release-age=" + seconds; } if (options.isEnablePnpm()) { // pnpm: minimumReleaseAge is a setting (in minutes), so it has // to be passed via the --config. CLI form, not as a // top-level option long minutes = (long) days * 24 * 60; - return Optional.of("--config.minimum-release-age=" + minutes); + return "--config.minimum-release-age=" + minutes; } if (npmSupportsMinReleaseAge) { // npm 11.10+: --min-release-age takes a value in days - return Optional.of("--min-release-age=" + days); + return "--min-release-age=" + days; } // Older npm: --before takes any Date.parse-able string String before = Instant.now().minus(days, ChronoUnit.DAYS).toString(); - return Optional.of("--before=" + before); + return "--before=" + before; } private void consumeProcessOutput(Process process, 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 28c04c3ebaa..92e1ceabd7b 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 @@ -767,94 +767,82 @@ private void assumeNPMIsInUse() { } @Test - void minimumFrontendPackageAge_zeroDisablesCheck_returnsEmpty() { - Options npmOptions = new MockOptions(npmFolder); - assertFalse(TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument(npmOptions, 0, true) - .isPresent()); - assertFalse(TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument(npmOptions, 0, false) - .isPresent()); - assertFalse(TaskRunNpmInstall.getMinimumFrontendPackageAgeArgument( - new MockOptions(npmFolder).withEnablePnpm(true), 0, false) - .isPresent()); - assertFalse(TaskRunNpmInstall.getMinimumFrontendPackageAgeArgument( - new MockOptions(npmFolder).withEnableBun(true), 0, false) - .isPresent()); - } - - @Test - void minimumFrontendPackageAge_npmNewEnough_addsMinReleaseAgeArgument() { - Options npmOptions = new MockOptions(npmFolder); - Optional arg = TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument(npmOptions, 2, true); + void minimumFrontendPackageAge_npmNewEnough_usesMinReleaseAgeArgument() { // npm 11.10+: --min-release-age takes a value in days - assertEquals("--min-release-age=2", arg.orElseThrow()); + assertEquals("--min-release-age=2", + TaskRunNpmInstall.getMinimumFrontendPackageAgeArgument( + new MockOptions(npmFolder), 2, true)); } @Test void minimumFrontendPackageAge_npmTooOld_fallsBackToBeforeArgument() { - Options npmOptions = new MockOptions(npmFolder); - Optional arg = TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument(npmOptions, 2, false); - assertTrue(arg.isPresent()); - assertTrue(arg.get().startsWith("--before="), - "Older npm should fall back to --before, was: " + arg.get()); + String arg = TaskRunNpmInstall.getMinimumFrontendPackageAgeArgument( + new MockOptions(npmFolder), 2, false); + assertTrue(arg.startsWith("--before="), + "Older npm should fall back to --before, was: " + arg); } @Test - void minimumFrontendPackageAge_pnpm_addsMinimumReleaseAgeArgument() { - Options pnpmOptions = new MockOptions(npmFolder).withEnablePnpm(true); - Optional arg = TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument(pnpmOptions, 2, false); + void minimumFrontendPackageAge_pnpm_usesMinimumReleaseAgeInMinutes() { // 2 days = 2880 minutes; pnpm setting form - assertEquals("--config.minimum-release-age=2880", arg.orElseThrow()); + assertEquals("--config.minimum-release-age=2880", + TaskRunNpmInstall.getMinimumFrontendPackageAgeArgument( + new MockOptions(npmFolder).withEnablePnpm(true), 2, + false)); } @Test - void minimumFrontendPackageAge_bun_addsMinimumReleaseAgeInSeconds() { - Options bunOptions = new MockOptions(npmFolder).withEnableBun(true); + void minimumFrontendPackageAge_bun_usesMinimumReleaseAgeInSeconds() { // 2 days = 172800 seconds - assertEquals("--minimum-release-age=172800", TaskRunNpmInstall - .getMinimumFrontendPackageAgeArgument(bunOptions, 2, false) - .orElseThrow()); + assertEquals("--minimum-release-age=172800", + TaskRunNpmInstall.getMinimumFrontendPackageAgeArgument( + new MockOptions(npmFolder).withEnableBun(true), 2, + false)); } @Test void resolveMinimumFrontendPackageAge_nothingConfigured_usesDefault() { - FrontendTools tools = Mockito.mock(FrontendTools.class); - Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), - Mockito.anyString(), Mockito.any())) - .thenReturn(Optional.empty()); + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); - assertEquals( - TaskRunNpmInstall.DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS, - resolveMinimumFrontendPackageAge(new MockOptions(npmFolder), - tools)); + assertEquals("--min-release-age=" + + TaskRunNpmInstall.DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS, + resolveMinimumFrontendPackageAgeArgument( + new MockOptions(npmFolder), tools).orElseThrow()); + } + + @Test + void resolveMinimumFrontendPackageAge_zeroConfigured_noArgument() { + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); + + assertFalse(resolveMinimumFrontendPackageAgeArgument( + new MockOptions(npmFolder).withMinimumFrontendPackageAgeDays(0), + tools).isPresent()); } @Test void resolveMinimumFrontendPackageAge_npmrcValue_doesNotOverrideIt() { - FrontendTools tools = Mockito.mock(FrontendTools.class); + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), Mockito.eq("min-release-age"), Mockito.eq(npmFolder))) .thenReturn(Optional.of("7")); - // 0 means no argument is passed, so npm applies its own configuration - assertEquals(0, resolveMinimumFrontendPackageAge( - new MockOptions(npmFolder), tools)); + // No argument is passed, so npm applies its own configuration + assertFalse(resolveMinimumFrontendPackageAgeArgument( + new MockOptions(npmFolder), tools).isPresent()); } @Test void resolveMinimumFrontendPackageAge_configuredInVaadin_overridesNpmrcValue() { - FrontendTools tools = Mockito.mock(FrontendTools.class); + FrontendTools tools = mockToolsWithoutMinimumReleaseAge(); Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), Mockito.eq("min-release-age"), Mockito.eq(npmFolder))) .thenReturn(Optional.of("7")); - assertEquals(3, resolveMinimumFrontendPackageAge( - new MockOptions(npmFolder).withMinimumFrontendPackageAgeDays(3), - tools)); + assertEquals("--min-release-age=3", + resolveMinimumFrontendPackageAgeArgument( + new MockOptions(npmFolder) + .withMinimumFrontendPackageAgeDays(3), + tools).orElseThrow()); } @Test @@ -866,16 +854,25 @@ void resolveMinimumFrontendPackageAge_bunfigValue_doesNotOverrideIt() minimumReleaseAge = 604800 """); - assertEquals(0, - resolveMinimumFrontendPackageAge( - new MockOptions(npmFolder).withEnableBun(true), - Mockito.mock(FrontendTools.class))); + assertFalse(resolveMinimumFrontendPackageAgeArgument( + new MockOptions(npmFolder).withEnableBun(true), + mockToolsWithoutMinimumReleaseAge()).isPresent()); + } + + private FrontendTools mockToolsWithoutMinimumReleaseAge() { + FrontendTools tools = Mockito.mock(FrontendTools.class); + Mockito.when(tools.npmSupportsMinReleaseAge(Mockito.anyList())) + .thenReturn(true); + Mockito.when(tools.getConfiguredSetting(Mockito.anyList(), + Mockito.anyString(), Mockito.any())) + .thenReturn(Optional.empty()); + return tools; } - private int resolveMinimumFrontendPackageAge(Options options, - FrontendTools tools) { - return TaskRunNpmInstall.resolveMinimumFrontendPackageAgeDays(options, - tools, List.of("npm"), true, + private Optional resolveMinimumFrontendPackageAgeArgument( + Options options, FrontendTools tools) { + return TaskRunNpmInstall.resolveMinimumFrontendPackageAgeArgument( + options, tools, List.of("npm"), LoggerFactory.getLogger(TaskRunNpmInstallTest.class)); }