diff --git a/flow-data/src/main/java/com/vaadin/flow/data/provider/DataCommunicator.java b/flow-data/src/main/java/com/vaadin/flow/data/provider/DataCommunicator.java index e6d78a9231c..b29d3b13fb4 100644 --- a/flow-data/src/main/java/com/vaadin/flow/data/provider/DataCommunicator.java +++ b/flow-data/src/main/java/com/vaadin/flow/data/provider/DataCommunicator.java @@ -610,8 +610,10 @@ public T getItem(int index) { * because the backend can have the item on that index (we simply * not yet fetched this item during the scrolling). */ - return (T) getDataProvider().fetch(buildQuery(index, 1)).findFirst() - .orElse(null); + try (Stream stream = getDataProvider() + .fetch(buildQuery(index, 1))) { + return stream.findFirst().orElse(null); + } } } @@ -1010,18 +1012,20 @@ protected Stream fetchFromProvider(int offset, int limit) { int page = 0; do { final int newOffset = offset + page * pageSize; - Stream dataProviderStream = doFetchFromDataProvider( - newOffset, pageSize); - // Stream.Builder is not thread safe, so for parallel stream - // we need to first collect items before adding them - if (dataProviderStream.isParallel()) { - getLogger().debug( - "Data provider {} has returned parallel stream on 'fetch' call", - getDataProvider().getClass()); - dataProviderStream.collect(Collectors.toList()) - .forEach(addItemAndCheckConsumer); - } else { - dataProviderStream.forEach(addItemAndCheckConsumer); + try (Stream dataProviderStream = doFetchFromDataProvider( + newOffset, pageSize)) { + // Stream.Builder is not thread safe, so for parallel + // stream we need to first collect items before adding + // them + if (dataProviderStream.isParallel()) { + getLogger().debug( + "Data provider {} has returned parallel stream on 'fetch' call", + getDataProvider().getClass()); + dataProviderStream.collect(Collectors.toList()) + .forEach(addItemAndCheckConsumer); + } else { + dataProviderStream.forEach(addItemAndCheckConsumer); + } } page++; } while (page < pages @@ -1040,8 +1044,10 @@ protected Stream fetchFromProvider(int offset, int limit) { getLogger().debug( "Data provider {} has returned parallel stream on 'fetch' call", getDataProvider().getClass()); - stream = stream.collect(Collectors.toList()).stream(); - assert !stream.isParallel(); + try (Stream parallelStream = stream) { + stream = parallelStream.collect(Collectors.toList()).stream(); + assert !stream.isParallel(); + } } SizeVerifier verifier = new SizeVerifier<>(limit); @@ -1476,17 +1482,20 @@ private Activation activate(Range range) { // XXX Explicitly refresh anything that is updated List activeKeys = new ArrayList<>(range.length()); - fetchFromProvider(range.getStart(), range.length()).forEach(bean -> { - boolean mapperHasKey = keyMapper.has(bean); - String key = keyMapper.key(bean); - if (mapperHasKey) { - // Ensure latest instance from provider is used - keyMapper.refresh(bean); - passivatedByUpdate.values().stream() - .forEach(set -> set.remove(key)); - } - activeKeys.add(key); - }); + try (Stream stream = fetchFromProvider(range.getStart(), + range.length())) { + stream.forEach(bean -> { + boolean mapperHasKey = keyMapper.has(bean); + String key = keyMapper.key(bean); + if (mapperHasKey) { + // Ensure latest instance from provider is used + keyMapper.refresh(bean); + passivatedByUpdate.values().stream() + .forEach(set -> set.remove(key)); + } + activeKeys.add(key); + }); + } boolean needsSizeRecheck = activeKeys.size() < range.length(); return new Activation(activeKeys, needsSizeRecheck); } diff --git a/flow-data/src/main/java/com/vaadin/flow/data/provider/DataView.java b/flow-data/src/main/java/com/vaadin/flow/data/provider/DataView.java index a2bf6dfea90..b3a4cc9e04d 100644 --- a/flow-data/src/main/java/com/vaadin/flow/data/provider/DataView.java +++ b/flow-data/src/main/java/com/vaadin/flow/data/provider/DataView.java @@ -59,6 +59,22 @@ public interface DataView extends Serializable { /** * Get the full data available to the component. Data is filtered and sorted * the same way as in the component. + *

+ * Consumers of the returned stream are responsible for closing it when all + * the stream operations are done to ensure that any resources feeding the + * stream are properly released. Failure to close the stream might lead to + * resource leaks. + *

+ * It is strongly recommended to use a try-with-resources block to + * automatically close the stream after its terminal operation has been + * executed. Below is an example of how to properly use and close the + * stream: + * + *

{@code
+     * try (Stream stream = dataView.getItems()) {
+     *     stream.forEach(System.out::println); // Example terminal operation
+     * }
+     * }
* * @return filtered and sorted data set */ diff --git a/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalCommunicationController.java b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalCommunicationController.java index 5e111a671f8..416cb98bb37 100644 --- a/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalCommunicationController.java +++ b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalCommunicationController.java @@ -255,16 +255,18 @@ private List activate(Range range) { // XXX Explicitly refresh anything that is updated List activeKeys = new ArrayList<>(range.length()); - fetchItems.apply(parentKey, range).forEach(bean -> { - boolean mapperHasKey = keyMapper.has(bean); - String key = keyMapper.key(bean); - if (mapperHasKey) { - // Ensure latest instance from provider is used - keyMapper.refresh(bean); - passivatedByUpdate.values().forEach(set -> set.remove(key)); - } - activeKeys.add(key); - }); + try (Stream stream = fetchItems.apply(parentKey, range)) { + stream.forEach(bean -> { + boolean mapperHasKey = keyMapper.has(bean); + String key = keyMapper.key(bean); + if (mapperHasKey) { + // Ensure latest instance from provider is used + keyMapper.refresh(bean); + passivatedByUpdate.values().forEach(set -> set.remove(key)); + } + activeKeys.add(key); + }); + } return activeKeys; } diff --git a/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchyMapper.java b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchyMapper.java index b47bd6fd9b8..ae8cae9a57a 100644 --- a/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchyMapper.java +++ b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchyMapper.java @@ -534,8 +534,9 @@ private Stream getFlatChildrenStream(T parent) { private Stream getFlatChildrenStream(T parent, boolean includeParent) { List childList = Collections.emptyList(); if (isExpanded(parent)) { - childList = doFetchDirectChildren(parent) - .collect(Collectors.toList()); + try (Stream stream = doFetchDirectChildren(parent)) { + childList = stream.collect(Collectors.toList()); + } if (childList.isEmpty()) { removeChildren(parent == null ? null : getDataProvider().getId(parent)); @@ -563,8 +564,9 @@ private Stream getChildrenStream(T parent, Range range, boolean includeParent) { List childList = Collections.emptyList(); if (isExpanded(parent)) { - childList = doFetchDirectChildren(parent, range) - .collect(Collectors.toList()); + try (Stream stream = doFetchDirectChildren(parent, range)) { + childList = stream.collect(Collectors.toList()); + } if (childList.isEmpty()) { removeChildren(parent == null ? null : getDataProvider().getId(parent)); diff --git a/flow-data/src/test/java/com/vaadin/flow/data/provider/DataCommunicatorTest.java b/flow-data/src/test/java/com/vaadin/flow/data/provider/DataCommunicatorTest.java index c3a92b323f3..f6e575dc49e 100644 --- a/flow-data/src/test/java/com/vaadin/flow/data/provider/DataCommunicatorTest.java +++ b/flow-data/src/test/java/com/vaadin/flow/data/provider/DataCommunicatorTest.java @@ -996,6 +996,18 @@ public void getItem_withUndefinedSizeAndSorting() { dataCommunicator.getItem(2)); } + @Test + public void getItem_streamIsClosed() { + AtomicBoolean streamIsClosed = new AtomicBoolean(); + dataCommunicator.setDataProvider(createDataProvider(streamIsClosed), + null); + + fakeClientCommunication(); + dataCommunicator.getItem(0); + + Assert.assertTrue(streamIsClosed.get()); + } + @Test public void itemCountEstimateAndStep_defaults() { Assert.assertEquals(dataCommunicator.getItemCountEstimate(), @@ -1353,6 +1365,18 @@ public void fetchFromProvider_itemCountLessThanTwoPages_correctItemsReturned() { } + @Test + public void fetchFromProvider_streamIsClosed() { + AtomicBoolean streamIsClosed = new AtomicBoolean(); + dataCommunicator.setDataProvider(createDataProvider(streamIsClosed), + null); + dataCommunicator.setRequestedRange(0, 50); + + fakeClientCommunication(); + + Assert.assertTrue(streamIsClosed.get()); + } + @Test public void fetchEnabled_getItemCount_stillReturnsItemsCount() { dataCommunicator.setFetchEnabled(false); @@ -1737,6 +1761,11 @@ public Stream fetch(Query query) { } private AbstractDataProvider createDataProvider() { + return createDataProvider(new AtomicBoolean()); + } + + private AbstractDataProvider createDataProvider( + AtomicBoolean streamIsClosed) { return new AbstractDataProvider() { @Override public boolean isInMemory() { @@ -1752,7 +1781,8 @@ public int size(Query query) { public Stream fetch(Query query) { return asParallelIfRequired(IntStream.range(query.getOffset(), query.getLimit() + query.getOffset())) - .mapToObj(Item::new); + .mapToObj(Item::new) + .onClose(() -> streamIsClosed.set(true)); } }; } diff --git a/flow-data/src/test/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalCommunicatorDataTest.java b/flow-data/src/test/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalCommunicatorDataTest.java index 74142f79714..4274e2d2207 100644 --- a/flow-data/src/test/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalCommunicatorDataTest.java +++ b/flow-data/src/test/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalCommunicatorDataTest.java @@ -18,7 +18,9 @@ import java.io.Serializable; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.IntStream; +import java.util.stream.Stream; import org.junit.Assert; import org.junit.Before; @@ -221,6 +223,33 @@ public void uniqueKeyProviderIsNotSet_keysGeneratedByKeyMapper() { communicator.getKeyMapper().get(i))); } + @Test + public void expandRoot_streamIsClosed() { + AtomicBoolean streamIsClosed = new AtomicBoolean(); + + dataProvider = new TreeDataProvider<>(treeData) { + + @Override + public Stream fetchChildren( + HierarchicalQuery> query) { + return super.fetchChildren(query) + .onClose(() -> streamIsClosed.set(true)); + } + }; + + communicator.setDataProvider(dataProvider, null); + + communicator.expand(ROOT); + fakeClientCommunication(); + + communicator.setParentRequestedRange(0, 50, ROOT); + fakeClientCommunication(); + + communicator.reset(); + + Assert.assertTrue(streamIsClosed.get()); + } + @Test public void expandRoot_filterOutAllChildren_clearCalled() { parentClearCalled = false; diff --git a/flow-data/src/test/java/com/vaadin/flow/data/provider/hierarchy/HierarchyMapperWithDataTest.java b/flow-data/src/test/java/com/vaadin/flow/data/provider/hierarchy/HierarchyMapperWithDataTest.java index 6da7a8e211e..fe31e5ee0db 100644 --- a/flow-data/src/test/java/com/vaadin/flow/data/provider/hierarchy/HierarchyMapperWithDataTest.java +++ b/flow-data/src/test/java/com/vaadin/flow/data/provider/hierarchy/HierarchyMapperWithDataTest.java @@ -21,6 +21,7 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -288,6 +289,42 @@ public void getExpandedItems_tryToAddItemsToCollection_shouldThrowException() { expandedItems.add(new TreeNode("third-1")); } + @Test + public void fetchHierarchyItems_streamIsClosed() { + AtomicBoolean streamIsClosed = new AtomicBoolean(); + mapper = new HierarchyMapper<>(new TreeDataProvider<>(data) { + @Override + public Stream fetchChildren( + HierarchicalQuery> query) { + return super.fetchChildren(query) + .onClose(() -> streamIsClosed.set(true)); + } + }); + Node rootNode = testData.get(0); + mapper.expand(rootNode); + mapper.fetchHierarchyItems(rootNode, Range.between(0, 10)).count(); + + Assert.assertTrue(streamIsClosed.get()); + } + + @Test + public void fetchChildItems_streamIsClosed() { + AtomicBoolean streamIsClosed = new AtomicBoolean(); + mapper = new HierarchyMapper<>(new TreeDataProvider<>(data) { + @Override + public Stream fetchChildren( + HierarchicalQuery> query) { + return super.fetchChildren(query) + .onClose(() -> streamIsClosed.set(true)); + } + }); + Node rootNode = testData.get(0); + mapper.expand(rootNode); + mapper.fetchChildItems(rootNode, Range.between(0, 10)); + + Assert.assertTrue(streamIsClosed.get()); + } + private void expand(Node node) { insertRows(mapper.expand(node, mapper.getIndexOf(node).orElse(null))); } diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/StateNode.java b/flow-server/src/main/java/com/vaadin/flow/internal/StateNode.java index 766151d1429..156d6aa2814 100644 --- a/flow-server/src/main/java/com/vaadin/flow/internal/StateNode.java +++ b/flow-server/src/main/java/com/vaadin/flow/internal/StateNode.java @@ -793,6 +793,11 @@ private void doSetTree(StateTree tree) { } else { id = -1; } + } else if (id > -1 && getOwner() == NullOwner.get()) { + // When id is set but owner is NullOwner, removeFromTree has been + // called, so we should clear node id and attached state, thus allow + // moving the node to another StateTree + reset(false); } owner = tree; } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java index 26231fd2c59..1c6be44657b 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java @@ -13,6 +13,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -449,8 +450,30 @@ public static boolean hashAndBundleModulesEqual(JsonObject statsJson, } private static boolean versionAccepted(String expected, String actual) { - FrontendVersion expectedVersion = new FrontendVersion(expected); - FrontendVersion actualVersion = new FrontendVersion(actual); + FrontendVersion expectedVersion; + try { + expectedVersion = new FrontendVersion(expected); + } catch (NumberFormatException ex) { + expectedVersion = null; + } + FrontendVersion actualVersion; + try { + actualVersion = new FrontendVersion(actual); + } catch (NumberFormatException ex) { + actualVersion = null; + } + + if (expectedVersion == null && actualVersion == null) { + return Objects.equals(expected, actual); + } else if (expectedVersion == null || actualVersion == null) { + // expected or actual version is referencing a local package + // while the other one is a parsable version + getLogger().debug( + "Version '{}' cannot be parsed and compared to '{}'", + expectedVersion == null ? expected : actual, + expectedVersion == null ? actual : expected); + return false; + } if (expected.startsWith("~")) { boolean correctRange = expectedVersion diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateReactFiles.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateReactFiles.java index 06fc9dd4c39..785e9f89e60 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateReactFiles.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateReactFiles.java @@ -31,6 +31,7 @@ import com.vaadin.experimental.FeatureFlags; import com.vaadin.flow.internal.UsageStatistics; +import com.vaadin.flow.router.Route; import com.vaadin.flow.server.Constants; import com.vaadin.flow.server.ExecutionFailedException; import com.vaadin.flow.server.Version; @@ -59,7 +60,7 @@ */ public class TaskGenerateReactFiles implements FallibleCommand { - private final File frontendDirectory; + private Options options; protected static String NO_IMPORT = """ Faulty configuration of serverSideRoutes. The server route definition is missing from the '%1$s' file @@ -93,11 +94,12 @@ public class TaskGenerateReactFiles implements FallibleCommand { * the task options */ TaskGenerateReactFiles(Options options) { - this.frontendDirectory = options.getFrontendDirectory(); + this.options = options; } @Override public void execute() throws ExecutionFailedException { + File frontendDirectory = options.getFrontendDirectory(); File appTsx = new File(frontendDirectory, "App.tsx"); File flowTsx = new File( new File(frontendDirectory, FrontendUtils.GENERATED), @@ -114,9 +116,8 @@ public void execute() throws ExecutionFailedException { } else { String routesContent = FileUtils.readFileToString(routesTsx, UTF_8); - Pattern serverImport = Pattern.compile( - "import[\\s\\S]?\\{[\\s\\S]?serverSideRoutes[\\s\\S]?\\}[\\s\\S]?from[\\s\\S]?(\"|'|`)Frontend\\/generated\\/flow\\/Flow\\1;"); - if (!serverImport.matcher(routesContent).find()) { + if (missingServerImport(routesContent) + && serverRoutesAvailable()) { throw new ExecutionFailedException( String.format(NO_IMPORT, routesTsx.getPath())); } @@ -130,6 +131,17 @@ public void execute() throws ExecutionFailedException { } } + private boolean missingServerImport(String routesContent) { + Pattern serverImport = Pattern.compile( + "import[\\s\\S]?\\{[\\s\\S]?serverSideRoutes[\\s\\S]?\\}[\\s\\S]?from[\\s\\S]?(\"|'|`)Frontend\\/generated\\/flow\\/Flow\\1;"); + return !serverImport.matcher(routesContent).find(); + } + + private boolean serverRoutesAvailable() { + return !options.getClassFinder().getAnnotatedClasses(Route.class) + .isEmpty(); + } + private void writeFile(File target, String content) throws ExecutionFailedException { diff --git a/flow-server/src/test/java/com/vaadin/flow/dom/ElementTest.java b/flow-server/src/test/java/com/vaadin/flow/dom/ElementTest.java index 3ff2ce5ab5d..0add1e5744c 100644 --- a/flow-server/src/test/java/com/vaadin/flow/dom/ElementTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/dom/ElementTest.java @@ -2047,6 +2047,19 @@ public void testDetachEvent_stateTreeCanFound() { Assert.assertEquals(1, detached.get()); } + @Test + public void testMoveFromUiToUi_doesNotThrow() { + Element body = new UI().getElement(); + Element child = ElementFactory.createDiv(); + body.appendChild(child); + + child.removeFromTree(); + + body = new UI().getElement(); + body.appendChild(child); + Assert.assertEquals(body, child.getParent()); + } + @Test public void testRemoveFromTree_inDetachListener_removedFromParent() { Element body = new UI().getElement(); diff --git a/flow-server/src/test/java/com/vaadin/flow/internal/StateTreeTest.java b/flow-server/src/test/java/com/vaadin/flow/internal/StateTreeTest.java index 6dc7b8de51e..b012a99a2b9 100644 --- a/flow-server/src/test/java/com/vaadin/flow/internal/StateTreeTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/internal/StateTreeTest.java @@ -170,6 +170,20 @@ public void moveNodeToOtherRoot_throws() { StateNodeTest.setParent(node, anotherTree.getRootNode()); } + @Test + public void moveNodeToOtherRoot_removeFromTree_doesNotThrow() { + StateNode node = StateNodeTest.createEmptyNode(); + + StateNodeTest.setParent(node, tree.getRootNode()); + + node.removeFromTree(); + + StateTree anotherTree = new StateTree(new UI().getInternals(), + ElementChildrenList.class); + + StateNodeTest.setParent(node, anotherTree.getRootNode()); + } + @Test public void testNoRootAttachChange() { List changes = collectChangesExceptChildrenAddRemove(); diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/BundleValidationTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/BundleValidationTest.java index 28fe2bbf33e..36f4eef430b 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/BundleValidationTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/BundleValidationTest.java @@ -39,6 +39,7 @@ import elemental.json.Json; import elemental.json.JsonArray; import elemental.json.JsonObject; + import static com.vaadin.flow.server.Constants.DEV_BUNDLE_JAR_PATH; import static com.vaadin.flow.server.Constants.PROD_BUNDLE_JAR_PATH; @@ -1795,6 +1796,91 @@ public void flowFrontendPackageInPackageJson_noBundleRebuild() needsBuild); } + @Test + public void localPackageInPackageJson_notChanged_noBundleRebuild() + throws IOException { + createPackageJsonStub( + "{\"dependencies\": {\"my-pkg\": \"file:my-pkg\"}, \"vaadin\": { \"hash\": \"aHash\"} }"); + + final FrontendDependenciesScanner depScanner = Mockito + .mock(FrontendDependenciesScanner.class); + + JsonObject stats = getBasicStats(); + stats.getObject(PACKAGE_JSON_DEPENDENCIES).put("my-pkg", "file:my-pkg"); + + setupFrontendUtilsMock(stats); + + boolean needsBuild = BundleValidationUtil.needsBuild(options, + depScanner, finder, mode); + Assert.assertFalse( + "Shouldn't re-bundle when referencing local packages in package.json", + needsBuild); + } + + @Test + public void localPackageInPackageJson_differentReference_bundleRebuild() + throws IOException { + createPackageJsonStub( + "{\"dependencies\": {\"my-pkg\": \"file:my-pkg\"}, \"vaadin\": { \"hash\": \"aHash\"} }"); + + final FrontendDependenciesScanner depScanner = Mockito + .mock(FrontendDependenciesScanner.class); + + JsonObject stats = getBasicStats(); + stats.getObject(PACKAGE_JSON_DEPENDENCIES).put("my-pkg", + "./another-folder"); + + setupFrontendUtilsMock(stats); + + boolean needsBuild = BundleValidationUtil.needsBuild(options, + depScanner, finder, mode); + Assert.assertTrue( + "Should re-bundle when local packages have different values", + needsBuild); + } + + @Test + public void localPackageInPackageJson_parsableVersionInStats_bundleRebuild() + throws IOException { + createPackageJsonStub( + "{\"dependencies\": {\"my-pkg\": \"file:my-pkg\"}, \"vaadin\": { \"hash\": \"aHash\"} }"); + + final FrontendDependenciesScanner depScanner = Mockito + .mock(FrontendDependenciesScanner.class); + + JsonObject stats = getBasicStats(); + stats.getObject(PACKAGE_JSON_DEPENDENCIES).put("my-pkg", "1.0.0"); + + setupFrontendUtilsMock(stats); + + boolean needsBuild = BundleValidationUtil.needsBuild(options, + depScanner, finder, mode); + Assert.assertTrue( + "Should re-bundle when local package in package.json but parsable version in stats", + needsBuild); + } + + @Test + public void localPackageInStats_parsableVersionInPackageJson_bundleRebuild() + throws IOException { + createPackageJsonStub( + "{\"dependencies\": {\"my-pkg\": \"1.0.0\"}, \"vaadin\": { \"hash\": \"aHash\"} }"); + + final FrontendDependenciesScanner depScanner = Mockito + .mock(FrontendDependenciesScanner.class); + + JsonObject stats = getBasicStats(); + stats.getObject(PACKAGE_JSON_DEPENDENCIES).put("my-pkg", "file:my-pkg"); + + setupFrontendUtilsMock(stats); + + boolean needsBuild = BundleValidationUtil.needsBuild(options, + depScanner, finder, mode); + Assert.assertTrue( + "Should re-bundle when local package in stats but parsable version in package.json", + needsBuild); + } + @Test public void bundleMissesSomeEntries_devMode_skipBundleBuildSet_noBundleRebuild() throws IOException { diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskGenerateReactFilesTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskGenerateReactFilesTest.java index ae131ee1b58..cca70ca068c 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskGenerateReactFilesTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskGenerateReactFilesTest.java @@ -19,6 +19,7 @@ import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Collections; import org.apache.commons.io.FileUtils; import org.junit.Assert; @@ -28,8 +29,12 @@ import org.junit.rules.TemporaryFolder; import org.mockito.Mockito; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; import com.vaadin.flow.di.Lookup; +import com.vaadin.flow.router.Route; import com.vaadin.flow.server.ExecutionFailedException; +import com.vaadin.flow.server.frontend.scanner.ClassFinder; public class TaskGenerateReactFilesTest { @@ -38,11 +43,19 @@ public class TaskGenerateReactFilesTest { Options options; File routesTsx, frontend; + ClassFinder classFinder; @Before public void setup() throws IOException { - options = new Options(Mockito.mock(Lookup.class), - temporaryFolder.getRoot()).withBuildDirectory("target"); + classFinder = Mockito.mock(ClassFinder.class); + Lookup lookup = Mockito.mock(Lookup.class); + Mockito.when(lookup.lookup(ClassFinder.class)).thenReturn(classFinder); + + Mockito.when(classFinder.getAnnotatedClasses(Route.class)) + .thenReturn(Collections.singleton(TestRoute.class)); + + options = new Options(lookup, temporaryFolder.getRoot()) + .withBuildDirectory("target"); frontend = temporaryFolder.newFolder("frontend"); options.withFrontendDirectory(frontend); routesTsx = new File(frontend, "routes.tsx"); @@ -176,6 +189,44 @@ public void routesContainsRoutesExport_noExpectionThrown() task.execute(); } + @Test + public void missingImport_noServerRoutesDefined_noExpectionThrown() + throws IOException, ExecutionFailedException { + String content = """ + import HelloWorldView from 'Frontend/views/helloworld/HelloWorldView.js'; + import MainLayout from 'Frontend/views/MainLayout.js'; + import { lazy } from 'react'; + import { createBrowserRouter, RouteObject } from 'react-router-dom'; + import {protectRoutes} from "@hilla/react-auth"; + import LoginView from "Frontend/views/LoginView"; + + const AboutView = lazy(async () => import('Frontend/views/about/AboutView.js')); + + export const routes: RouteObject[] = protectRoutes([ + { + element: , + handle: { title: 'Main' }, + children: [ + { path: '/', element: , handle: { title: 'Hello World', rolesAllowed: ['USER'] } }, + { path: '/about', element: , handle: { title: 'About' } } + ], + }, + { path: '/login', element: }, + ]); + + export default createBrowserRouter(routes); + """; + + FileUtils.write(routesTsx, content, StandardCharsets.UTF_8); + + Mockito.when(classFinder.getAnnotatedClasses(Route.class)) + .thenReturn(Collections.emptySet()); + + TaskGenerateReactFiles task = new TaskGenerateReactFiles(options); + + task.execute(); + } + @Test public void routesexportMissing_expectionThrown() throws IOException { String content = """ @@ -214,4 +265,9 @@ public void routesexportMissing_expectionThrown() throws IOException { Assert.assertEquals(TaskGenerateReactFiles.MISSING_ROUTES_EXPORT, exception.getMessage()); } + + @Tag("div") + @Route("test") + private class TestRoute extends Component { + } } \ No newline at end of file diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/springnative/VaadinHintsRegistrar.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/springnative/VaadinHintsRegistrar.java index fedafa5324e..b9413988fcc 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/springnative/VaadinHintsRegistrar.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/springnative/VaadinHintsRegistrar.java @@ -23,6 +23,7 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { // Bundles, build info etc hints.resources().registerPattern("META-INF/VAADIN/*"); + hints.resources().registerPattern("vaadin-i18n/*"); // Random classes that need reflection for (String cls : getClasses()) {