Skip to content

Commit 61d4f94

Browse files
authored
fix: Ignore bundling for Lumo utilities in live updater (#22889)
* fix: Ignore bundling for Lumo utilities in live updater * Apply suggestion from @mshabarov * fix the pattern and add external links skip * add base protocol and test
1 parent 8a3f23f commit 61d4f94

File tree

2 files changed

+121
-4
lines changed

2 files changed

+121
-4
lines changed

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818
import java.io.Closeable;
1919
import java.io.File;
2020
import java.io.IOException;
21+
import java.net.URI;
2122
import java.util.ArrayList;
2223
import java.util.List;
2324
import java.util.Objects;
2425
import java.util.Optional;
2526
import java.util.Set;
27+
import java.util.regex.Pattern;
2628

2729
import org.slf4j.Logger;
2830
import org.slf4j.LoggerFactory;
@@ -52,7 +54,8 @@
5254
* For internal use only. May be renamed or removed in a future release.
5355
*/
5456
public class PublicResourcesLiveUpdater implements Closeable {
55-
57+
private final static Pattern THEME_URLS_PATTERN = Pattern
58+
.compile("^(.*/)?(lumo|aura)/.+\\.css$");
5659
private final List<FileWatcher> watchers = new ArrayList<>();
5760
private final List<File> roots = new ArrayList<>();
5861
private final VaadinContext context;
@@ -123,8 +126,8 @@ private FileWatcher getFileWatcher(File root, BrowserLiveReload liveReload)
123126
return;
124127
}
125128
for (String url : activeUrls) {
126-
if (isVaadinThemeUrl(url)) {
127-
// ignore Aura and Lumo urls
129+
if (isVaadinThemeUrl(url) || isExternalUrl(url)) {
130+
// ignore external urls, and Aura and Lumo urls
128131
continue;
129132
}
130133
String normalized = PublicStyleSheetBundler
@@ -154,9 +157,22 @@ private FileWatcher getFileWatcher(File root, BrowserLiveReload liveReload)
154157
return watcher;
155158
}
156159

160+
private boolean isExternalUrl(String url) {
161+
if (url.startsWith(ApplicationConstants.CONTEXT_PROTOCOL_PREFIX)
162+
|| url.startsWith(ApplicationConstants.BASE_PROTOCOL_PREFIX)) {
163+
return false;
164+
}
165+
try {
166+
return URI.create(url).isAbsolute();
167+
} catch (IllegalArgumentException e) {
168+
return true;
169+
}
170+
}
171+
157172
private boolean isVaadinThemeUrl(String url) {
158173
url = FrontendUtils.getUnixPath(new File(url).toPath());
159-
return url.contains("lumo/lumo.css") || url.contains("aura/aura.css");
174+
// all known urls from Aura and Lumo classes
175+
return THEME_URLS_PATTERN.matcher(url).matches();
160176
}
161177

162178
private Logger getLogger() {

vaadin-dev-server/src/test/java/com/vaadin/base/devserver/PublicResourcesLiveUpdaterTest.java

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import java.io.File;
1919
import java.nio.charset.StandardCharsets;
2020
import java.nio.file.Files;
21+
import java.util.Arrays;
22+
import java.util.HashSet;
2123
import java.util.List;
2224
import java.util.Optional;
2325
import java.util.Set;
@@ -30,13 +32,15 @@
3032
import org.mockito.MockedStatic;
3133
import org.mockito.Mockito;
3234

35+
import com.vaadin.flow.di.Lookup;
3336
import com.vaadin.flow.internal.ActiveStyleSheetTracker;
3437
import com.vaadin.flow.internal.BrowserLiveReload;
3538
import com.vaadin.flow.internal.BrowserLiveReloadAccessor;
3639
import com.vaadin.flow.server.VaadinContext;
3740
import com.vaadin.flow.server.startup.ApplicationConfiguration;
3841

3942
import static org.junit.Assert.assertTrue;
43+
import static org.mockito.ArgumentMatchers.anyString;
4044
import static org.mockito.ArgumentMatchers.argThat;
4145
import static org.mockito.ArgumentMatchers.eq;
4246

@@ -293,4 +297,101 @@ public void cssChange_ignoresVaadinThemeUrls_noLiveReloadUpdates()
293297
});
294298
}
295299
}
300+
301+
@Test
302+
public void vaadinThemeUrls_areNotBundled_whenCssChangesDetected()
303+
throws Exception {
304+
// Prepare a temp directory to watch
305+
File root = temporaryFolder.newFolder("watched-root");
306+
307+
// Mocks for Vaadin context and configuration (dev mode)
308+
VaadinContext ctx = Mockito.mock(VaadinContext.class);
309+
ApplicationConfiguration config = Mockito
310+
.mock(ApplicationConfiguration.class);
311+
Mockito.when(config.isProductionMode()).thenReturn(false);
312+
313+
// Mock BrowserLiveReload plumbing via Lookup
314+
Lookup lookup = Mockito.mock(Lookup.class);
315+
BrowserLiveReload liveReload = Mockito.mock(BrowserLiveReload.class);
316+
BrowserLiveReloadAccessor accessor = Mockito
317+
.mock(BrowserLiveReloadAccessor.class);
318+
Mockito.when(accessor.getLiveReload(ctx)).thenReturn(liveReload);
319+
Mockito.when(lookup.lookup(BrowserLiveReloadAccessor.class))
320+
.thenReturn(accessor);
321+
Mockito.when(ctx.getAttribute(eq(Lookup.class))).thenReturn(lookup);
322+
323+
// Mock Static: ApplicationConfiguration.get(context)
324+
try (MockedStatic<ApplicationConfiguration> appConfig = Mockito
325+
.mockStatic(ApplicationConfiguration.class);
326+
// Mock Static:
327+
// PublicStyleSheetBundler.forResourceLocations(...)
328+
MockedStatic<PublicStyleSheetBundler> bundlerStatic = Mockito
329+
.mockStatic(PublicStyleSheetBundler.class)) {
330+
331+
appConfig.when(() -> ApplicationConfiguration.get(Mockito.any()))
332+
.thenReturn(config);
333+
334+
// Active URLs include two Lumo URLs and one regular URL
335+
Set<String> activeUrls = new HashSet<>(Arrays.asList(
336+
"/lumo/utility.css", "lumo/utility.css",
337+
"/lumo/presets/compact.css", "lumo/presets/compact.css",
338+
"/aura/aura.css", "aura/aura.css", "/css/app.css",
339+
"context://css/app.css", "base://css/app.css",
340+
"http://localhost:8080/hello"));
341+
ActiveStyleSheetTracker tracker = Mockito
342+
.mock(ActiveStyleSheetTracker.class);
343+
Mockito.when(tracker.getActiveUrls()).thenReturn(activeUrls);
344+
// Ensure context returns our tracker for attribute-based access
345+
Mockito.when(ctx.getAttribute(
346+
Mockito.eq(ActiveStyleSheetTracker.class), Mockito.any()))
347+
.thenReturn(tracker);
348+
349+
// Bundler mock
350+
PublicStyleSheetBundler bundler = Mockito
351+
.mock(PublicStyleSheetBundler.class);
352+
// Return some Optional content for non-Lumo URLs to allow
353+
// liveReload.update
354+
Mockito.when(bundler.bundle(anyString(), anyString()))
355+
.thenReturn(Optional.of("css"));
356+
bundlerStatic
357+
.when(() -> PublicStyleSheetBundler
358+
.forResourceLocations(Mockito.anyList()))
359+
.thenReturn(bundler);
360+
361+
// Start the updater watching the temp root
362+
try (PublicResourcesLiveUpdater ignored = new PublicResourcesLiveUpdater(
363+
List.of(root.getAbsolutePath()), ctx)) {
364+
// Trigger a CSS change under the watched root
365+
File changed = new File(root, "trigger.css");
366+
Files.writeString(changed.toPath(), "body{}\n");
367+
368+
// Wait until the non-Lumo URL is processed (bundled at least
369+
// once)
370+
Awaitility.await().untilAsserted(() -> {
371+
Mockito.verify(bundler, Mockito.times(1))
372+
.bundle(eq("/css/app.css"), anyString());
373+
Mockito.verify(bundler, Mockito.times(1))
374+
.bundle(eq("context://css/app.css"), anyString());
375+
Mockito.verify(bundler, Mockito.times(1))
376+
.bundle(eq("base://css/app.css"), anyString());
377+
});
378+
379+
// Verify bundler.bundle was NEVER called for Lumo URLs
380+
Mockito.verify(bundler, Mockito.never())
381+
.bundle(eq("/lumo/utility.css"), anyString());
382+
Mockito.verify(bundler, Mockito.never())
383+
.bundle(eq("lumo/utility.css"), anyString());
384+
Mockito.verify(bundler, Mockito.never())
385+
.bundle(eq("/lumo/presets/compact.css"), anyString());
386+
Mockito.verify(bundler, Mockito.never())
387+
.bundle(eq("lumo/presets/compact.css"), anyString());
388+
Mockito.verify(bundler, Mockito.never())
389+
.bundle(eq("/aura/aura.css"), anyString());
390+
Mockito.verify(bundler, Mockito.never())
391+
.bundle(eq("aura/aura.css"), anyString());
392+
Mockito.verify(bundler, Mockito.never())
393+
.bundle(eq("http://localhost:8080/hello"), anyString());
394+
}
395+
}
396+
}
296397
}

0 commit comments

Comments
 (0)