Skip to content

refactor!: isolate CssImport styles within exported web component #21362

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,12 @@
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -57,6 +55,7 @@
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
import com.vaadin.flow.shared.ApplicationConstants;
import com.vaadin.flow.theme.AbstractTheme;
import com.vaadin.flow.theme.ThemeDefinition;

import static com.vaadin.flow.server.Constants.COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT;
import static com.vaadin.flow.server.Constants.PACKAGE_JSON;
Expand All @@ -73,30 +72,6 @@
*/
abstract class AbstractUpdateImports implements Runnable {

private static final String CSS_PREPARE = "function addCssBlock(block) {\n"
+ " const tpl = document.createElement('template');\n"
+ " tpl.innerHTML = block;\n"
+ " document.head.appendChild(tpl.content);\n" + "}";
private static final String IMPORT_INJECT = "import { injectGlobalCss } from 'Frontend/generated/jar-resources/theme-util.js';\n";
private static final String IMPORT_WC_INJECT = "import { injectGlobalWebcomponentCss } from 'Frontend/generated/jar-resources/theme-util.js';\n";

private static final String CSS_IMPORT = "import $cssFromFile_%d from '%s';%n";
private static final String CSS_IMPORT_AND_MAKE_LIT_CSS = CSS_IMPORT
+ "const $css_%1$d = typeof $cssFromFile_%1$d === 'string' ? unsafeCSS($cssFromFile_%1$d) : $cssFromFile_%1$d;";
private static final String CSS_PRE = CSS_IMPORT_AND_MAKE_LIT_CSS + "%n"
+ "addCssBlock(`";
private static final String CSS_POST = "`);";
private static final String CSS_BASIC_TPL = CSS_PRE
+ "<style%s>${$css_%1$d}</style>" + CSS_POST;
private static final String INJECT_CSS = CSS_IMPORT
+ "%ninjectGlobalCss($cssFromFile_%1$d.toString(), 'CSSImport end', document);%n";
private static final Pattern INJECT_CSS_PATTERN = Pattern
.compile("^\\s*injectGlobalCss\\(([^,]+),.*$");
private static final String INJECT_WC_CSS = "injectGlobalWebcomponentCss(%s);";

private static final String THEMABLE_MIXIN_IMPORT = "import { css, unsafeCSS, registerStyles } from '@vaadin/vaadin-themable-mixin';";
private static final String REGISTER_STYLES_FOR_TEMPLATE = CSS_IMPORT_AND_MAKE_LIT_CSS
+ "%n" + "registerStyles('%s', $css_%1$d%s);";
static final String RESET_FOCUS_JS = "() => {\n"
+ " let ae=document.activeElement;\n"
+ " while(ae&&ae.shadowRoot) ae = ae.shadowRoot.activeElement;\n"
Expand Down Expand Up @@ -160,8 +135,6 @@ public void run() {

Map<File, List<String>> output = process(css, javascript);
writeOutput(output);
writeWebComponentImports(
filterWebComponentImports(output.get(generatedFlowImports)));

getLogger().debug("Imports and chunks update took {} ms.",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
Expand Down Expand Up @@ -223,44 +196,6 @@ protected void writeOutput(Map<File, List<String>> outputFiles) {
}
}

// Visible for test
List<String> filterWebComponentImports(List<String> lines) {
if (lines != null) {
// Exclude Lumo global imports for exported web-component
List<String> copy = new ArrayList<>(lines);
copy.add(0, IMPORT_WC_INJECT);
copy.removeIf(VAADIN_LUMO_GLOBAL_IMPORT.asPredicate());
// Add global CSS imports with a per-webcomponent registration
final ListIterator<String> li = copy.listIterator();
while (li.hasNext()) {
adaptCssInjectForWebComponent(li, li.next());
}
return copy;
}
return lines;
}

private void adaptCssInjectForWebComponent(ListIterator<String> iterator,
String line) {
Matcher matcher = INJECT_CSS_PATTERN.matcher(line);
if (matcher.matches()) {
iterator.add(String.format(INJECT_WC_CSS, matcher.group(1)));
}
}

private void writeWebComponentImports(List<String> lines) {
if (lines != null) {
try {
generatedFilesSupport.writeIfChanged(
generatedFlowWebComponentImports, lines);
} catch (IOException e) {
throw new IllegalStateException(
"Failed to update the generated Flow imports for exported web component",
e);
}
}
}

/**
* Processes what the scanner found and produces a set of files to write to
* the generated folder.
Expand All @@ -280,7 +215,7 @@ private Map<File, List<String>> process(Map<ChunkInfo, List<CssData>> css,

Map<ChunkInfo, List<String>> lazyJavascript = new LinkedHashMap<>();
List<String> eagerJavascript = new ArrayList<>();
Map<ChunkInfo, List<String>> lazyCss = new LinkedHashMap<>();
Map<ChunkInfo, List<CssData>> lazyCssData = new LinkedHashMap<>();
List<CssData> eagerCssData = new ArrayList<>();
for (Entry<ChunkInfo, List<String>> entry : javascript.entrySet()) {
if (isLazyRoute(entry.getKey())) {
Expand All @@ -294,10 +229,7 @@ private Map<File, List<String>> process(Map<ChunkInfo, List<CssData>> css,
boolean hasThemeFor = entry.getValue().stream()
.anyMatch(cssData -> cssData.getThemefor() != null);
if (isLazyRoute(entry.getKey()) && !hasThemeFor) {
List<String> cssLines = getCssLines(entry.getValue());
if (!cssLines.isEmpty()) {
lazyCss.put(entry.getKey(), cssLines);
}
lazyCssData.put(entry.getKey(), entry.getValue());
} else {
eagerCssData.addAll(entry.getValue());
}
Expand All @@ -308,15 +240,15 @@ private Map<File, List<String>> process(Map<ChunkInfo, List<CssData>> css,

List<String> chunkLoader = new ArrayList<>();

if (!lazyJavascript.isEmpty() || !lazyCss.isEmpty()) {
if (!lazyJavascript.isEmpty() || !lazyCssData.isEmpty()) {
getLogger().debug("Start generating lazy loaded chunks.");
start = System.nanoTime();

chunkLoader.add("");
chunkLoader.add("const loadOnDemand = (key) => {");
chunkLoader.add(" const pending = [];");
Set<ChunkInfo> mergedChunkKeys = merge(lazyJavascript.keySet(),
lazyCss.keySet());
lazyCssData.keySet());
Set<String> processedChunkHashes = new HashSet<>(
mergedChunkKeys.size());

Expand All @@ -326,11 +258,9 @@ private Map<File, List<String>> process(Map<ChunkInfo, List<CssData>> css,
chunkLines.addAll(
getModuleLines(lazyJavascript.get(chunkInfo)));
}
boolean hasLazyCss = lazyCss.containsKey(chunkInfo);
boolean hasLazyCss = lazyCssData.containsKey(chunkInfo);
if (hasLazyCss) {
chunkLines.add(IMPORT_INJECT);
chunkLines.add(THEMABLE_MIXIN_IMPORT);
chunkLines.addAll(lazyCss.get(chunkInfo));
chunkLines.addAll(getCssLines(lazyCssData.get(chunkInfo)));
}

if (chunkLines.isEmpty()) {
Expand Down Expand Up @@ -373,27 +303,27 @@ private Map<File, List<String>> process(Map<ChunkInfo, List<CssData>> css,
List<String> mainLines = new ArrayList<>();

// Convert eager CSS data to JS and deduplicate it
List<String> mainCssLines = getCssLines(eagerCssData);
if (!mainCssLines.isEmpty()) {
mainLines.add(IMPORT_INJECT);
mainLines.add(THEMABLE_MIXIN_IMPORT);
mainLines.addAll(mainCssLines);
}
mainLines.addAll(getModuleLines(eagerJavascript));

// Move all imports to the top
List<String> copy = new ArrayList<>(mainLines);
copy.removeIf(line -> !line.startsWith("import "));
mainLines.removeIf(line -> line.startsWith("import "));
mainLines.addAll(0, copy);

mainLines.addAll(getCssLines(eagerCssData));
mainLines.addAll(chunkLoader);
mainLines.add("window.Vaadin = window.Vaadin || {};");
mainLines.add("window.Vaadin.Flow = window.Vaadin.Flow || {};");
mainLines.add("window.Vaadin.Flow.loadOnDemand = loadOnDemand;");
mainLines.add("window.Vaadin.Flow.resetFocus = " + RESET_FOCUS_JS);

List<String> webComponentLines = new ArrayList<>();
webComponentLines.addAll(getModuleLines(eagerJavascript));
webComponentLines.addAll(getCssLines(eagerCssData, "*"));
webComponentLines.add("window.Vaadin = window.Vaadin || {};");
webComponentLines.add("window.Vaadin.Flow = window.Vaadin.Flow || {};");
webComponentLines.add(
"window.Vaadin.Flow.loadOnDemand = () => Promise.resolve(0);");
webComponentLines
.add("window.Vaadin.Flow.resetFocus = " + RESET_FOCUS_JS);
webComponentLines.removeIf(VAADIN_LUMO_GLOBAL_IMPORT.asPredicate());

files.put(generatedFlowImports, mainLines);
files.put(generatedFlowWebComponentImports, webComponentLines);
files.put(generatedFlowDefinitions,
Collections.singletonList("export {}"));

Expand Down Expand Up @@ -469,16 +399,19 @@ String resolveGeneratedModule(String module) {
* @return the JS statements needed to import and apply the CSS data
*/
protected List<String> getCssLines(List<CssData> css) {
return getCssLines(css, null);
}

protected List<String> getCssLines(List<CssData> css,
String exportedWebComponent) {
List<String> lines = new ArrayList<>();

Set<String> cssNotFound = new HashSet<>();
LinkedHashSet<CssData> allCss = new LinkedHashSet<>(css);
int i = 0;
for (CssData cssData : allCss) {
if (!addCssLines(lines, cssData, i)) {
if (!addCssLines(lines, cssData, exportedWebComponent)) {
cssNotFound.add(cssData.getValue());
}
i++;
}

if (!cssNotFound.isEmpty()) {
Expand Down Expand Up @@ -787,52 +720,43 @@ private boolean isFileOrDirectory(File base, String... path) {
* @return true if the imported CSS files does exist, false otherwise
*/
protected boolean addCssLines(Collection<String> lines, CssData cssData,
int i) {
String exportedWebComponent) {
String cssFile = resolveResource(cssData.getValue());
boolean found = importedFileExists(cssFile);
String cssImport = toValidBrowserImport(cssFile);
// Without this, Vite adds the CSS also to the document
cssImport += "?inline";

Map<String, String> optionalsMap = new LinkedHashMap<>();
if (cssData.getInclude() != null) {
optionalsMap.put("include", cssData.getInclude());
}
Map<String, String> query = new HashMap<>();
query.put("path", cssImport);

if (cssData.getId() != null && cssData.getThemefor() != null) {
throw new IllegalStateException(
"provide either id or themeFor for @CssImport of resource "
+ cssData.getValue() + ", not both");
}
if (cssData.getThemefor() != null) {
query.put("theme-for", cssData.getThemefor());
}
if (cssData.getInclude() != null) {
query.put("include", cssData.getInclude());
}
if (cssData.getId() != null) {
optionalsMap.put("moduleId", cssData.getId());
} else if (cssData.getThemefor() != null) {
optionalsMap.put("moduleId", getThemeIdPrefix() + "_" + i);
}
String optionals = "";
if (!optionalsMap.isEmpty()) {
optionals = ", " + optionalsMap.keySet().stream()
.map(k -> k + ": '" + optionalsMap.get(k) + "'")
.collect(Collectors.joining(", ", "{", "}"));
}

if (cssData.getThemefor() != null || cssData.getId() != null) {
String themeFor = cssData.getThemefor() != null
? cssData.getThemefor()
: "";
addLines(lines, String.format(REGISTER_STYLES_FOR_TEMPLATE, i,
cssImport, themeFor, optionals));
} else if (cssData.getInclude() != null) {
if (!lines.contains("function addCssBlock(block) {")) {
addLines(lines, CSS_PREPARE);
}
String include = cssData.getInclude() != null
? " include=\"" + cssData.getInclude() + "\""
: "";
addLines(lines,
String.format(CSS_BASIC_TPL, i, cssImport, include));
} else {
addLines(lines, String.format(INJECT_CSS, i, cssImport));
query.put("module-id", cssData.getId());
}
if (exportedWebComponent != null) {
query.put("exported-web-component", exportedWebComponent);
}

String queryString = query.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));

// Prevent Vite from treating
// `virtual:flow-css-import?path=path/to/file.css` as css import
queryString += "&_";

lines.add(
"import 'virtual:flow-css-import?%s';".formatted(queryString));

return found || !options.isBundleBuild();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ protected void writeOutput(Map<File, List<String>> outputFiles) {

@Override
protected boolean addCssLines(Collection<String> lines, CssData cssData,
int i) {
super.addCssLines(lines, cssData, i);
String exportedWebComponent) {
super.addCssLines(lines, cssData, exportedWebComponent);
// CSS files in 'generated/jar-resources' are not generated at this
// moment, so not let the application interrupt and continue with
// checking the dev-bundle
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const EXPORTED_WEB_COMPONENT_SELECTOR = Symbol('exported-web-component-selector');

const exportedWebComponents = new Set();
const exportedWebComponentStyleSheets = new Map();

/**
* Helper function to add a style sheet to the shadow root of an element.
*/
function addAdoptedStyleSheet(element, styleSheet) {
const { shadowRoot } = element;
if (!shadowRoot.adoptedStyleSheets.includes(styleSheet)) {
shadowRoot.adoptedStyleSheets.push(styleSheet);
}
}

/**
* Helper function to remove a style sheet from the shadow root of an element.
*/
function removeAdoptedStyleSheet(element, styleSheet) {
const { shadowRoot } = element;
shadowRoot.adoptedStyleSheets = shadowRoot.adoptedStyleSheets.filter((ss) => ss !== styleSheet);
}

export function injectExportedWebComponentCSS(id, content, { selector }) {
let styleSheet = exportedWebComponentStyleSheets.get(id);
if (!styleSheet) {
styleSheet = new CSSStyleSheet();
exportedWebComponentStyleSheets.set(id, styleSheet);
}

styleSheet[EXPORTED_WEB_COMPONENT_SELECTOR] = selector;

// replaceSync will automatically update the stylesheet in
// all shadow roots that have adopted it without needing to
// notify each shadow root individually.
styleSheet.replaceSync(content);

// Add the stylesheet to the shadow root of existing exported
// web component instances that match the selector. Note,
// if the selector is *, the stylesheet will be added to
// all shadow roots.
exportedWebComponents.forEach((component) => {
if (component.matches(selector)) {
addAdoptedStyleSheet(component, styleSheet);
} else {
removeAdoptedStyleSheet(component, styleSheet);
}
});
}

export function exportedWebComponentConnected(component) {
exportedWebComponents.add(component);

// Add the stylesheet to the shadow root of the component
// if it matches the selector.
exportedWebComponentStyleSheets.forEach((styleSheet) => {
if (component.matches(styleSheet[EXPORTED_WEB_COMPONENT_SELECTOR])) {
addAdoptedStyleSheet(component, styleSheet);
}
});
}

export function exportedWebComponentDisconnected(component) {
exportedWebComponents.delete(component);

// Remove all previously added stylesheets from the shadow root.
exportedWebComponentStyleSheets.forEach((styleSheet) => {
removeAdoptedStyleSheet(component, styleSheet);
});
}
Loading
Loading