Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,17 @@
/**
* Copies all frontend resources from JAR files into a given folder.
* <p>
* 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.
* <p>
* 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.
* <p>
* For internal use only. May be renamed or removed in a future release.
*
Expand All @@ -49,6 +57,7 @@ public class TaskCopyFrontendFiles
private static final String WILDCARD_INCLUSION_APP_THEME_JAR = "**/themes/**/*";
private final Options options;
private final Set<File> resourceLocations;
private final Set<File> warnedLegacyLocations = new HashSet<>();

/**
* Scans the jar files given defined by {@code resourcesToScan}.
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<LoggerFactory> 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<LoggerFactory> 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.<Object> any(),
Mockito.<Object> any(), Mockito.<Object> 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 {

Expand Down
Loading