diff --git a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskCopyFrontendFiles.java b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskCopyFrontendFiles.java index 8e8a2fa857a..71b0b54b203 100644 --- a/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskCopyFrontendFiles.java +++ b/flow-build-tools/src/main/java/com/vaadin/flow/server/frontend/TaskCopyFrontendFiles.java @@ -36,9 +36,17 @@ /** * Copies all frontend resources from JAR files into a given folder. *

- * The task considers "frontend resources" all files placed in - * {@literal META-INF/frontend}, {@literal META-INF/resources/frontend} and - * {@literal META-INF/resources/[**]/themes} folders. + * "Frontend resources" are bundle sources for {@code @JsModule} / + * {@code @CssImport} annotations. The recommended location for them in addon + * JARs is {@literal META-INF/frontend}. The legacy location + * {@literal META-INF/resources/frontend} is still scanned for backwards + * compatibility but is deprecated; a per-jar warning is emitted when it is + * used. Theme files under {@literal META-INF/resources/[**]/themes} are also + * copied. + *

+ * Public runtime resources for {@code @StyleSheet} / {@code @JavaScript} should + * be placed under {@literal META-INF/resources/} and are served directly by the + * servlet container — they are not handled by this task. *

* For internal use only. May be renamed or removed in a future release. * @@ -49,6 +57,7 @@ public class TaskCopyFrontendFiles private static final String WILDCARD_INCLUSION_APP_THEME_JAR = "**/themes/**/*"; private final Options options; private final Set resourceLocations; + private final Set warnedLegacyLocations = new HashSet<>(); /** * Scans the jar files given defined by {@code resourcesToScan}. @@ -85,10 +94,13 @@ public void execute() { .addAll(TaskCopyLocalFrontendFiles.copyLocalResources( new File(location, RESOURCES_FRONTEND_DEFAULT), targetDirectory)); + File legacyDir = new File(location, + COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT); + if (legacyDir.isDirectory()) { + warnAboutDeprecatedFrontendLayout(location); + } handledFiles.addAll(TaskCopyLocalFrontendFiles - .copyLocalResources(new File(location, - COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT), - targetDirectory)); + .copyLocalResources(legacyDir, targetDirectory)); // copies from resources, but excludes already copied from // resources/frontend handledFiles @@ -100,6 +112,10 @@ public void execute() { .copyIncludedFilesFromJarTrimmingBasePath(location, RESOURCES_FRONTEND_DEFAULT, targetDirectory, "**/*")); + if (jarContentsManager.containsPath(location, + COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT + "/")) { + warnAboutDeprecatedFrontendLayout(location); + } handledFiles.addAll(jarContentsManager .copyIncludedFilesFromJarTrimmingBasePath(location, COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT, @@ -136,4 +152,16 @@ private Logger log() { return LoggerFactory.getLogger(this.getClass()); } + private void warnAboutDeprecatedFrontendLayout(File location) { + if (warnedLegacyLocations.add(location)) { + log().warn("Addon '{}' contains frontend sources under {}/. " + + "This location is deprecated; migrate them to {}/ " + + "(bundle sources for @JsModule/@CssImport) or to " + + "META-INF/resources/ (runtime resources for " + + "@StyleSheet/@JavaScript).", location.getName(), + COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT, + RESOURCES_FRONTEND_DEFAULT); + } + } + } diff --git a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskCopyFrontendFilesTest.java b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskCopyFrontendFilesTest.java index 54ccec7336b..a47428efcee 100644 --- a/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskCopyFrontendFilesTest.java +++ b/flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/TaskCopyFrontendFilesTest.java @@ -26,12 +26,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import tools.jackson.databind.JsonNode; import com.vaadin.flow.testutil.TestUtils; import com.vaadin.tests.util.MockOptions; +import static com.vaadin.flow.server.Constants.COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT; import static com.vaadin.flow.server.Constants.PACKAGE_JSON; +import static com.vaadin.flow.server.Constants.RESOURCES_FRONTEND_DEFAULT; import static com.vaadin.flow.server.Constants.TARGET; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -94,6 +100,58 @@ void should_createPackageJson() throws IOException { assertFalse(deps.has(NodeUpdater.DEP_NAME_FLOW_JARS)); } + @Test + void should_warnOnceAboutLegacyLayout_whenJarUsesLegacyLocation() { + File legacyJar = TestUtils + .getTestJar("jar-with-frontend-resources.jar"); + TaskCopyFrontendFiles task = taskFor(legacyJar); + + Logger logger = Mockito.spy(Logger.class); + try (MockedStatic loggerFactoryMocked = Mockito + .mockStatic(LoggerFactory.class)) { + loggerFactoryMocked + .when(() -> LoggerFactory.getLogger(task.getClass())) + .thenReturn(logger); + + // Two runs of the same task instance should still warn only once + task.execute(); + task.execute(); + + Mockito.verify(logger, Mockito.times(1)).warn( + Mockito.contains("deprecated"), + Mockito.eq(legacyJar.getName()), + Mockito.eq(COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT), + Mockito.eq(RESOURCES_FRONTEND_DEFAULT)); + } + } + + @Test + void should_notWarnAboutLegacyLayout_whenJarUsesModernLocation() { + TaskCopyFrontendFiles task = taskFor( + TestUtils.getTestJar("jar-with-modern-frontend.jar")); + + Logger logger = Mockito.spy(Logger.class); + try (MockedStatic loggerFactoryMocked = Mockito + .mockStatic(LoggerFactory.class)) { + loggerFactoryMocked + .when(() -> LoggerFactory.getLogger(task.getClass())) + .thenReturn(logger); + + task.execute(); + + Mockito.verify(logger, Mockito.never()).warn( + Mockito.contains("deprecated"), Mockito. any(), + Mockito. any(), Mockito. any()); + } + } + + private TaskCopyFrontendFiles taskFor(File... locations) { + Options options = new MockOptions(null); + options.withJarFrontendResourcesFolder(frontendDepsFolder) + .copyResources(jars(locations)); + return new TaskCopyFrontendFiles(options); + } + private void should_collectJsAndCssFilesFromJars(String jarFile, String fsDir) throws IOException {