Skip to content

Commit

Permalink
fix: handle shadow DOM stylesheets in production and dev bundle (#17776
Browse files Browse the repository at this point in the history
…) (CP: 24.2) (#17797)

* fix: handle shadow DOM stylesheets in production and dev bundle (#17776)

* fix: handle shadow DOM stylesheets in production and dev bundle

The legacy shadow DOM stylesheets that can potentially be present in the
'theme/<themeName>/components/' folder are not considered when deciding
if a new bundle needs to be created, so the application may miss custom
components styles.
This change checks for existence of theme components CSS, and it triggers
a bundle rebuild if any is found.

Fixes #16407

* apply review suggestions

* apply review suggestions

* fixed parent pom version

---------

Co-authored-by: Marco Collovati <marco@vaadin.com>
  • Loading branch information
vaadin-bot and mcollovati committed Oct 6, 2023
1 parent 2aad4fa commit f33a446
Show file tree
Hide file tree
Showing 27 changed files with 708 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand Down Expand Up @@ -35,13 +34,11 @@
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
import com.vaadin.flow.server.webcomponent.WebComponentExporterTagExtractor;
import com.vaadin.flow.server.webcomponent.WebComponentExporterUtils;
import com.vaadin.pro.licensechecker.BuildType;
import com.vaadin.pro.licensechecker.LicenseChecker;
import com.vaadin.pro.licensechecker.Product;

import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;

import static com.vaadin.flow.server.Constants.DEV_BUNDLE_JAR_PATH;

/**
Expand Down Expand Up @@ -202,6 +199,14 @@ private static boolean needsBuildInternal(Options options,
return true;
}

if (ThemeValidationUtil.themeShadowDOMStylesheetsChanged(options,
statsJson, frontendDependencies)) {
UsageStatistics.markAsUsed(
"flow/rebundle-reason-changed-shadow-DOM-stylesheets",
null);
return true;
}

if (BundleValidationUtil.exportedWebComponents(statsJson, finder)) {
UsageStatistics.markAsUsed(
"flow/rebundle-reason-added-exported-component", null);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
package com.vaadin.flow.server.frontend;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.vaadin.flow.server.frontend.scanner.ClassFinder;
import com.vaadin.flow.theme.ThemeDefinition;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.internal.JsonUtils;
import com.vaadin.flow.server.Constants;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
import com.vaadin.flow.theme.ThemeDefinition;

import elemental.json.Json;
import elemental.json.JsonArray;
Expand All @@ -38,6 +46,7 @@ public class ThemeValidationUtil {

private static final Pattern THEME_PATH_PATTERN = Pattern
.compile("themes\\/([\\s\\S]+?)\\/theme.json");
private static final String FRONTEND_HASHES_KEY = "frontendHashes";

public static boolean themeConfigurationChanged(Options options,
JsonObject statsJson,
Expand Down Expand Up @@ -86,7 +95,7 @@ public static boolean themeConfigurationChanged(Options options,
}

collectThemeJsonContentsInFrontend(options, themeJsonContents, key,
projectThemeJson.get(), finder);
projectThemeJson.get());
}

for (Map.Entry<String, JsonObject> themeContent : themeJsonContents
Expand Down Expand Up @@ -114,6 +123,84 @@ public static boolean themeConfigurationChanged(Options options,
return false;
}

/**
* Checks if theme has legacy Shadow DOM stylesheets in
* {@literal <theme>/components} folder and if their content has changed.
*
* @param options
* Flow plugin options
* @param statsJson
* the stats.json for the application bundle.
* @param frontendDependencies
* frontend dependencies scanner to lookup for theme settings
* @return {@literal true} if the theme has legacy Shadow DOM stylesheets,
* and they are not included on the application bundle, otherwise
* {@literal false}.
*/
public static boolean themeShadowDOMStylesheetsChanged(Options options,
JsonObject statsJson,
FrontendDependenciesScanner frontendDependencies) {
File frontendDirectory = options.getFrontendDirectory();
// Scan the theme hierarchy and collect all <theme>/components folders
Set<Path> themeComponentsDirs = Optional
.ofNullable(frontendDependencies.getThemeDefinition())
.map(ThemeDefinition::getName).filter(name -> !name.isBlank())
.map(themeName -> {
Map<String, JsonObject> themeJsonContents = new HashMap<>();
ThemeUtils.getThemeJson(themeName, frontendDirectory)
.ifPresent(
themeJson -> collectThemeJsonContentsInFrontend(
options, themeJsonContents,
themeName, themeJson));
return themeJsonContents.keySet().stream()
.map(name -> ThemeUtils
.getThemeFolder(frontendDirectory, name))
.map(dir -> new File(dir, "components"))
.filter(File::exists).map(File::toPath)
.collect(Collectors.toSet());
}).orElse(null);
if (themeComponentsDirs != null) {
Map<String, String> hashesWithNoComponentCssMatches = new HashMap<>();
if (statsJson.hasKey(FRONTEND_HASHES_KEY)) {
JsonObject json = statsJson.getObject(FRONTEND_HASHES_KEY);
Stream.of(json.keys())
// Only considers bundled resources located in
// '[generated/jar-resources/]themes/<themeName>/components'
.filter(path -> themeComponentsDirs.stream()
.anyMatch(dir -> frontendDirectory.toPath()
.resolve(path).startsWith(dir)))
.forEach(key -> hashesWithNoComponentCssMatches.put(key,
json.getString(key)));
}

List<String> themeComponentsCssFiles = new ArrayList<>();
for (Path dir : themeComponentsDirs) {
FileUtils.listFiles(dir.toFile(), new String[] { "css" }, true)
.stream()
.filter(themeFile -> isFrontendResourceChangedOrMissingInBundle(
hashesWithNoComponentCssMatches,
frontendDirectory, themeFile))
.map(f -> frontendDirectory.toPath()
.relativize(f.toPath()).toString())
.collect(Collectors
.toCollection(() -> themeComponentsCssFiles));
}
if (!themeComponentsCssFiles.isEmpty()) {
BundleValidationUtil.logChangedFiles(themeComponentsCssFiles,
"Detected new or changed theme components CSS files");
}
if (!hashesWithNoComponentCssMatches.isEmpty()) {
BundleValidationUtil.logChangedFiles(
new ArrayList<>(
hashesWithNoComponentCssMatches.keySet()),
"Detected removed theme components CSS files");
}
return !(themeComponentsCssFiles.isEmpty()
&& hashesWithNoComponentCssMatches.isEmpty());
}
return false;
}

private static boolean hasNewAssetsOrImports(JsonObject contentsInStats,
Map.Entry<String, JsonObject> themeContent) {
JsonObject json = themeContent.getValue();
Expand All @@ -128,7 +215,7 @@ private static boolean hasNewAssetsOrImports(JsonObject contentsInStats,

private static void collectThemeJsonContentsInFrontend(Options options,
Map<String, JsonObject> themeJsonContents, String themeName,
JsonObject themeJson, ClassFinder finder) {
JsonObject themeJson) {
Optional<String> parentThemeInFrontend = ThemeUtils
.getParentThemeName(themeJson);
if (parentThemeInFrontend.isPresent()) {
Expand All @@ -137,8 +224,7 @@ private static void collectThemeJsonContentsInFrontend(Options options,
parentThemeName, options.getFrontendDirectory());
parentThemeJson.ifPresent(
jsonObject -> collectThemeJsonContentsInFrontend(options,
themeJsonContents, parentThemeName, jsonObject,
finder));
themeJsonContents, parentThemeName, jsonObject));
}

themeJsonContents.put(themeName, themeJson);
Expand Down Expand Up @@ -287,6 +373,26 @@ && objectIncludesEntry(arrayIteratingEntry,
return allEntriesFound;
}

private static boolean isFrontendResourceChangedOrMissingInBundle(
Map<String, String> bundledHashes, File frontendFolder,
File frontendResource) {
String relativePath = frontendFolder.toPath()
.relativize(frontendResource.toPath()).toString();
boolean presentInBundle = bundledHashes.containsKey(relativePath);
if (presentInBundle) {
final String contentHash;
try {
contentHash = BundleValidationUtil.calculateHash(
FileUtils.readFileToString(frontendResource,
StandardCharsets.UTF_8));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return !bundledHashes.remove(relativePath).equals(contentHash);
}
return true;
}

private static Logger getLogger() {
return LoggerFactory.getLogger(ThemeValidationUtil.class);
}
Expand Down
9 changes: 7 additions & 2 deletions flow-server/src/main/resources/vite.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,12 +296,17 @@ function statsExtracterPlugin(): PluginOption {

const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map'];

const isThemeComponentsResource = (id: string) =>
id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))
&& id.match(/.*\/jar-resources\/themes\/[^\/]+\/components\//);

// collects project's frontend resources in frontend folder, excluding
// 'generated' sub-folder
// 'generated' sub-folder, except for legacy shadow DOM stylesheets
// packaged in `theme/components/` folder.
modules
.map((id) => id.replace(/\\/g, '/'))
.filter((id) => id.startsWith(frontendFolder.replace(/\\/g, '/')))
.filter((id) => !id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/')))
.filter((id) => !id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/')) || isThemeComponentsResource(id))
.map((id) => id.substring(frontendFolder.length + 1))
.map((line: string) => (line.includes('?') ? line.substring(0, line.lastIndexOf('?')) : line))
.forEach((line: string) => {
Expand Down

0 comments on commit f33a446

Please sign in to comment.