Skip to content

Commit 1cde734

Browse files
authored
fix: Populate ActiveStyleSheetTracker during page load for CSS live reload without HotSwap Agent (#23603) (#23635)
ActiveStyleSheetTracker was only populated by StyleSheetHotswapper.onInit(), which requires a HotSwap Agent. Without one, the tracker stayed empty and PublicResourcesLiveUpdater's file watcher silently skipped all CSS updates. Register active @Stylesheet URLs during normal dev-mode page loading: - AppShellRegistry.createSettings() tracks AppShell stylesheets - UIInternals.addComponentDependencies() tracks component stylesheets Both paths are guarded by !isProductionMode() for zero production overhead. Also simplify the live reload integration test to rely on the file watcher instead of manually triggering reload via button clicks. Fixes #23592
1 parent d97722c commit 1cde734

File tree

4 files changed

+78
-86
lines changed

4 files changed

+78
-86
lines changed

flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import com.vaadin.flow.dom.impl.BasicElementStateProvider;
5757
import com.vaadin.flow.function.DeploymentConfiguration;
5858
import com.vaadin.flow.function.SerializableConsumer;
59+
import com.vaadin.flow.internal.ActiveStyleSheetTracker;
5960
import com.vaadin.flow.internal.ConstantPool;
6061
import com.vaadin.flow.internal.JacksonCodec;
6162
import com.vaadin.flow.internal.StateNode;
@@ -1049,6 +1050,13 @@ public void addComponentDependencies(
10491050
dependencies.getStyleSheets().forEach(styleSheet -> page
10501051
.addStyleSheet(styleSheet.value(), styleSheet.loadMode()));
10511052

1053+
VaadinService service = session.getService();
1054+
if (!service.getDeploymentConfiguration().isProductionMode()) {
1055+
dependencies.getStyleSheets()
1056+
.forEach(styleSheet -> ActiveStyleSheetTracker.get(service)
1057+
.trackAddForComponent(styleSheet.value()));
1058+
}
1059+
10521060
warnForUnavailableBundledDependencies(componentClass, dependencies);
10531061
}
10541062

flow-server/src/main/java/com/vaadin/flow/server/AppShellRegistry.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.vaadin.flow.component.page.TargetElement;
4343
import com.vaadin.flow.component.page.Viewport;
4444
import com.vaadin.flow.function.DeploymentConfiguration;
45+
import com.vaadin.flow.internal.ActiveStyleSheetTracker;
4546
import com.vaadin.flow.router.PageTitle;
4647
import com.vaadin.flow.shared.ApplicationConstants;
4748
import com.vaadin.flow.theme.Theme;
@@ -232,6 +233,13 @@ private AppShellSettings createSettings(VaadinRequest request) {
232233
stylesheets.put(href, sheet.value());
233234
}
234235
}
236+
237+
if (!request.getService().getDeploymentConfiguration()
238+
.isProductionMode()) {
239+
ActiveStyleSheetTracker.get(request.getService())
240+
.trackForAppShell(stylesheets.values());
241+
}
242+
235243
addStyleSheets(request, stylesheets, settings);
236244
return settings;
237245
}

flow-tests/test-live-reload/src/main/java/com/vaadin/flow/uitest/ui/StylesheetLiveReloadView.java

Lines changed: 20 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -15,103 +15,47 @@
1515
*/
1616
package com.vaadin.flow.uitest.ui;
1717

18-
import java.io.File;
19-
import java.nio.file.Paths;
20-
import java.util.List;
21-
22-
import com.vaadin.base.devserver.PublicStyleSheetBundler;
2318
import com.vaadin.flow.component.dependency.StyleSheet;
2419
import com.vaadin.flow.component.html.Div;
25-
import com.vaadin.flow.component.html.NativeButton;
26-
import com.vaadin.flow.function.DeploymentConfiguration;
27-
import com.vaadin.flow.internal.BrowserLiveReloadAccessor;
2820
import com.vaadin.flow.router.Route;
29-
import com.vaadin.flow.server.VaadinRequest;
30-
import com.vaadin.flow.server.VaadinService;
3121
import com.vaadin.flow.uitest.servlet.ViewTestLayout;
3222

3323
@Route(value = "com.vaadin.flow.uitest.ui.StylesheetLiveReloadView", layout = ViewTestLayout.class)
3424
@StyleSheet("context://css/view/view.css")
3525
@StyleSheet("context://css/view/for-deletion.css")
3626
public class StylesheetLiveReloadView extends AbstractLiveReloadView {
3727

38-
private final PublicStyleSheetBundler bundler;
39-
4028
public StylesheetLiveReloadView() {
41-
DeploymentConfiguration configuration = VaadinService.getCurrent()
42-
.getDeploymentConfiguration();
43-
File projectFolder = configuration.getProjectFolder();
44-
String outputFolder = configuration.getBuildFolder() + "/classes";
45-
File root = Paths.get(projectFolder.getAbsolutePath(), outputFolder,
46-
"META-INF", "resources").toFile();
47-
bundler = PublicStyleSheetBundler.forResourceLocations(List.of(root));
48-
49-
add(makeDiv("appshell-style", "css/styles.css", "css/styles.css"));
50-
add(makeDiv("appshell-imported", "css/imported.css", "css/styles.css"));
29+
add(makeDiv("appshell-style", "css/styles.css"));
30+
add(makeDiv("appshell-imported", "css/imported.css"));
5131
add(makeDiv("appshell-nested-imported",
52-
"css/nested/nested-imported.css", "css/styles.css"));
53-
add(makeDiv("appshell-image", "css/images/gobo.png", "css/styles.css"));
54-
add(makeDiv("view-style", "css/view/view.css", "css/view/view.css"));
55-
add(makeDiv("view-imported", "css/view/imported.css",
56-
"css/view/view.css"));
32+
"css/nested/nested-imported.css"));
33+
add(makeDiv("appshell-image", "css/images/gobo.png"));
34+
add(makeDiv("view-style", "css/view/view.css"));
35+
add(makeDiv("view-imported", "css/view/imported.css"));
5736
add(makeDiv("view-nested-imported",
58-
"css/view/nested/nested-imported.css", "css/view/view.css"));
59-
add(makeDiv("view-image", "css/images/viking.png",
60-
"css/view/view.css"));
37+
"css/view/nested/nested-imported.css"));
38+
add(makeDiv("view-image", "css/images/viking.png"));
6139
add(makeDivForDelete());
6240
}
6341

64-
private Div makeDiv(String cssClass, String resourceFileToChange,
65-
String mainCssFile) {
42+
private Div makeDiv(String id, String resourceFilePath) {
6643
Div div = new Div();
67-
div.setId(cssClass);
68-
div.setText("Style defined in " + resourceFileToChange);
69-
div.addClassName(cssClass);
70-
71-
// Simulate Flow Hotswapper handling of CSS change
72-
NativeButton reloadButton = new NativeButton(
73-
"Trigger Stylesheet live reload", ev -> {
74-
String bundledCssContent = getContentForFile(mainCssFile);
75-
BrowserLiveReloadAccessor
76-
.getLiveReloadFromService(
77-
VaadinService.getCurrent())
78-
.ifPresent(reload -> reload.update(
79-
"context://" + mainCssFile,
80-
bundledCssContent));
81-
});
82-
reloadButton.setId("reload-" + cssClass);
83-
reloadButton.getElement().setAttribute("test-resource-file-path",
84-
resourceFileToChange);
85-
div.add(reloadButton);
44+
div.setId(id);
45+
div.setText("Style defined in " + resourceFilePath);
46+
div.addClassName(id);
47+
div.getElement().setAttribute("test-resource-file-path",
48+
resourceFilePath);
8649
return div;
8750
}
8851

8952
private Div makeDivForDelete() {
90-
// Separate element to test deletion of a stylesheet file
91-
Div deleteDiv = new Div();
92-
deleteDiv.setId("view-style-deleted");
93-
deleteDiv.setText("Style defined in css/view/view.css (delete test)");
94-
// Use the same class so initial style applies before deletion
95-
deleteDiv.addClassName("view-style-deleted");
96-
97-
NativeButton deleteButton = new NativeButton(
98-
"Trigger Stylesheet delete",
99-
ev -> BrowserLiveReloadAccessor
100-
.getLiveReloadFromService(VaadinService.getCurrent())
101-
.ifPresent(reload -> reload.update(
102-
"context://css/view/for-deletion.css", null)));
103-
deleteButton.setId("delete-view-style-deleted");
104-
deleteButton.getElement().setAttribute("test-resource-file-path",
53+
Div div = new Div();
54+
div.setId("view-style-deleted");
55+
div.setText("Style defined in css/view/for-deletion.css (delete test)");
56+
div.addClassName("view-style-deleted");
57+
div.getElement().setAttribute("test-resource-file-path",
10558
"css/view/for-deletion.css");
106-
deleteDiv.add(deleteButton);
107-
return deleteDiv;
108-
}
109-
110-
private String getContentForFile(String cssFile) {
111-
String contextPath = VaadinRequest.getCurrent() != null
112-
? VaadinRequest.getCurrent().getContextPath()
113-
: "";
114-
return bundler.bundle(cssFile, contextPath)
115-
.orElseThrow(AssertionError::new);
59+
return div;
11660
}
11761
}

flow-tests/test-live-reload/src/test/java/com/vaadin/flow/uitest/ui/StylesheetLiveReloadIT.java

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class StylesheetLiveReloadIT extends AbstractLiveReloadIT {
5454
private static final String DIV_BG_COLOR_BEFORE_DELETE = "rgba(0, 255, 0, 1)";
5555

5656
private Path resourcesPath;
57+
private Path sourceResourcesPath;
5758
private Path updatedImagePath;
5859

5960
@Before
@@ -67,6 +68,22 @@ public void detectStylesheetsLocation() throws URISyntaxException {
6768
resourcesPath = Paths.get(markerUrl.toURI()).getParent();
6869
updatedImagePath = resourcesPath
6970
.resolve(Paths.get("css", "images", "vaadin-logo.png"));
71+
72+
// The file watcher monitors the source directory, not the build
73+
// output. Walk up from the target path to find the Maven project
74+
// root (containing both "src" and "pom.xml"), then resolve the
75+
// source path.
76+
Path projectDir = resourcesPath;
77+
while (projectDir != null
78+
&& !(Files.isDirectory(projectDir.resolve("src")) && Files
79+
.isRegularFile(projectDir.resolve("pom.xml")))) {
80+
projectDir = projectDir.getParent();
81+
}
82+
Assert.assertNotNull(
83+
"Could not find project root from " + resourcesPath,
84+
projectDir);
85+
sourceResourcesPath = projectDir
86+
.resolve("src/main/resources/META-INF/resources");
7087
}
7188

7289
@After
@@ -186,12 +203,11 @@ private void triggerReloadStyleSheet(String styledDivID)
186203

187204
private void triggerReload(String divId, ThrowingConsumer<Path> updater)
188205
throws IOException {
189-
TestBenchElement button = $("button").id("reload-" + divId);
190-
String resourceRelativePath = button
206+
TestBenchElement div = $("div").id(divId);
207+
String resourceRelativePath = div
191208
.getDomAttribute("test-resource-file-path");
192209
Assert.assertNotNull(
193-
"No test-resource-file-path attribute found for button "
194-
+ button,
210+
"No test-resource-file-path attribute found for div " + divId,
195211
resourceRelativePath);
196212

197213
Path resourcePath = resourcesPath
@@ -206,7 +222,15 @@ private void triggerReload(String divId, ThrowingConsumer<Path> updater)
206222
// Make sure the servlet container returns the updated content
207223
waitUntilContentMatches(
208224
getRootURL() + "/context/" + resourceRelativePath, content);
209-
button.click();
225+
226+
// Also update the source file so the file watcher detects the
227+
// change and triggers a live CSS reload via the debug connection
228+
Path sourcePath = sourceResourcesPath
229+
.resolve(resourceRelativePath.replace('/', File.separatorChar));
230+
if (Files.exists(sourcePath)) {
231+
styleSheetRestore.put(sourcePath, Files.readAllBytes(sourcePath));
232+
updater.accept(sourcePath);
233+
}
210234
}
211235

212236
private void triggerReloadImage(String styledDivID) throws IOException {
@@ -218,12 +242,12 @@ private void triggerReloadImage(String styledDivID) throws IOException {
218242
}
219243

220244
private void triggerDelete() throws IOException {
221-
TestBenchElement button = $("button").id("delete-view-style-deleted");
222-
String resourceRelativePath = button
245+
TestBenchElement div = $("div").id("view-style-deleted");
246+
String resourceRelativePath = div
223247
.getDomAttribute("test-resource-file-path");
224248
Assert.assertNotNull(
225-
"No test-resource-file-path attribute found for button "
226-
+ button,
249+
"No test-resource-file-path attribute found for div "
250+
+ "view-style-deleted",
227251
resourceRelativePath);
228252

229253
Path resourcePath = resourcesPath
@@ -235,7 +259,15 @@ private void triggerDelete() throws IOException {
235259
Files.delete(resourcePath);
236260

237261
waitUntil(driver -> !Files.exists(resourcePath));
238-
button.click();
262+
263+
// Also delete the source file so the file watcher detects the
264+
// change and triggers a live CSS reload via the debug connection
265+
Path sourcePath = sourceResourcesPath
266+
.resolve(resourceRelativePath.replace('/', File.separatorChar));
267+
if (Files.exists(sourcePath)) {
268+
styleSheetRestore.put(sourcePath, Files.readAllBytes(sourcePath));
269+
Files.delete(sourcePath);
270+
}
239271
}
240272

241273
private void waitUntilContentMatches(String url, byte[] expectedContent) {

0 commit comments

Comments
 (0)