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.