Skip to content

Commit bd9b2cf

Browse files
mshabarovcaalador
andauthored
feat: Watch the CSS files in the project's public static resources folder (#22392)
Watches files in src/main/resources/META-INF/resources/ and other public static resources folders to support HMR for StyleSheet("foo.css"). --------- Co-authored-by: caalador <mikael.grankvist@vaadin.com>
1 parent 1515cc7 commit bd9b2cf

File tree

4 files changed

+380
-0
lines changed

4 files changed

+380
-0
lines changed

flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ protected Stream<String> getExcludedPatterns() {
9292
"com\\.vaadin\\.base\\.devserver\\.OpenInCurrentIde.*",
9393
"com\\.vaadin\\.base\\.devserver\\.RestartMonitor",
9494
"com\\.vaadin\\.base\\.devserver\\.ThemeLiveUpdater",
95+
"com\\.vaadin\\.base\\.devserver\\.PublicResourcesCssLiveUpdater",
9596
"com\\.vaadin\\.base\\.devserver\\.editor..*",
9697
"com\\.vaadin\\.base\\.devserver\\.themeeditor..*",
9798
"com\\.vaadin\\.base\\.devserver\\.util\\.BrowserLauncher",

vaadin-dev-server/src/main/java/com/vaadin/base/devserver/DevModeHandlerManagerImpl.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ public void initDevModeHandler(Set<Class<?>> classes, VaadinContext context)
114114
ApplicationConfiguration config = ApplicationConfiguration
115115
.get(context);
116116
startWatchingThemeFolder(context, config);
117+
startWatchingPublicResourcesCss(context, config);
117118
watchExternalDependencies(context, config);
118119
setFullyStarted(true);
119120
}, executorService);
@@ -245,4 +246,35 @@ private static Logger getLogger() {
245246
return LoggerFactory.getLogger(DevModeHandlerManagerImpl.class);
246247
}
247248

249+
// package-private for testing
250+
void startWatchingPublicResourcesCss(VaadinContext context,
251+
ApplicationConfiguration config) {
252+
final File projectFolder = config.getProjectFolder();
253+
List.of("src/main/resources/META-INF/resources",
254+
"src/main/resources/resources", "src/main/resources/static",
255+
"src/main/resources/public").stream().map(path -> {
256+
File resourcesFolder = new File(projectFolder, path);
257+
if (resourcesFolder.exists()) {
258+
return resourcesFolder.getAbsolutePath();
259+
}
260+
return null;
261+
}).filter(path -> path != null).forEach(path -> {
262+
try {
263+
File resourcesFolder = new File(path);
264+
if (!resourcesFolder.isDirectory()) {
265+
getLogger().debug(
266+
"No public resources folder found at {}",
267+
resourcesFolder);
268+
return;
269+
}
270+
registerWatcherShutdownCommand(
271+
new PublicResourcesCssLiveUpdater(
272+
resourcesFolder, context));
273+
} catch (Exception e) {
274+
getLogger().error(
275+
"Failed to start live-reload for public CSS resources",
276+
e);
277+
}
278+
});
279+
}
248280
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.base.devserver;
17+
18+
import java.io.Closeable;
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
22+
import java.nio.file.Files;
23+
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
27+
import com.vaadin.flow.internal.BrowserLiveReload;
28+
import com.vaadin.flow.internal.BrowserLiveReloadAccessor;
29+
import com.vaadin.flow.server.VaadinContext;
30+
31+
/**
32+
* Watches given resourcesFolder for CSS file changes and performs hot CSS
33+
* updates via BrowserLiveReload.
34+
*/
35+
class PublicResourcesCssLiveUpdater implements Closeable {
36+
37+
private final File resourcesFolder;
38+
private final VaadinContext context;
39+
private FileWatcher watcher;
40+
41+
PublicResourcesCssLiveUpdater(File resourcesFolder, VaadinContext context)
42+
throws IOException {
43+
this.resourcesFolder = resourcesFolder;
44+
this.context = context;
45+
initWatcher();
46+
}
47+
48+
private void initWatcher() throws IOException {
49+
if (resourcesFolder == null || !resourcesFolder.isDirectory()) {
50+
getLogger().debug(
51+
"Public resources folder {} not found, skipping watcher",
52+
resourcesFolder);
53+
return;
54+
}
55+
var liveReloadOpt = BrowserLiveReloadAccessor
56+
.getLiveReloadFromContext(context);
57+
if (liveReloadOpt.isEmpty()) {
58+
getLogger().debug(
59+
"Browser live reload not available, skipping public resources watcher");
60+
return;
61+
}
62+
BrowserLiveReload liveReload = liveReloadOpt.get();
63+
watcher = new FileWatcher(changed -> {
64+
try {
65+
if (changed.isFile() && changed.getName().endsWith(".css")) {
66+
// Path to be used from the browser: "/" + relative path
67+
String rel = resourcesFolder.toPath()
68+
.relativize(changed.toPath()).toString()
69+
.replace(File.separatorChar, '/');
70+
String browserPath = "/" + rel;
71+
String contents = Files.readString(changed.toPath(),
72+
StandardCharsets.UTF_8);
73+
liveReload.update(browserPath, contents);
74+
}
75+
} catch (Exception e) {
76+
getLogger().error(
77+
"Unable to perform hot update of public resource CSS "
78+
+ changed,
79+
e);
80+
try {
81+
liveReload.reload();
82+
} catch (Exception ignore) {
83+
// no-op
84+
}
85+
}
86+
}, resourcesFolder);
87+
watcher.start();
88+
getLogger().debug("Watching {} for public CSS changes",
89+
resourcesFolder);
90+
}
91+
92+
@Override
93+
public void close() throws IOException {
94+
if (watcher != null) {
95+
watcher.stop();
96+
watcher = null;
97+
}
98+
}
99+
100+
private Logger getLogger() {
101+
return LoggerFactory.getLogger(getClass());
102+
}
103+
}

0 commit comments

Comments
 (0)