Skip to content

Commit

Permalink
feat: Enable custom theme for pre-compiled prod bundle (#16751)
Browse files Browse the repository at this point in the history
* feat: Enable custom theme for pre-compiled prod bundle

* test: Add production profile to tests

* Use app shell registry to get theme

* fix compile error

* Add a pre-compiled flag, exclude config parameter

* Look for CSS files in classpath in prod mode

* Fix javadoc and add test modules to compute matrix

* Read theme name from file again, because it is needed for live reload

* Serve theme assets from pre-compiled prod bundle

* Test for checking style imports

* Make own modules for testing parent theme in prod

* New mode, use static resources parameter and refactor methods

* Fix compile errors

* Add missing themes segment

* Add static resources examples to the test page

* Add a test for static resources referenced with url(./foo.png)

* Fix formatting
  • Loading branch information
mshabarov committed May 12, 2023
1 parent 81df7d0 commit 281e90b
Show file tree
Hide file tree
Showing 44 changed files with 909 additions and 175 deletions.
Expand Up @@ -21,6 +21,7 @@
import java.nio.file.Paths;

import com.vaadin.flow.internal.hilla.EndpointRequestUtil;
import com.vaadin.flow.server.frontend.BundleUtils;
import com.vaadin.flow.server.frontend.FileIOUtils;
import com.vaadin.flow.server.frontend.FrontendUtils;

Expand Down Expand Up @@ -61,12 +62,14 @@ default boolean frontendHotdeploy() {
/**
* Gets the mode the application is running in.
*
* @return production, development using livereload or development using
* bundle
* @return custom production bundle, pre-compiled production bundle,
* development using livereload or development using bundle
**/
default Mode getMode() {
if (isProductionMode()) {
return Mode.PRODUCTION;
return BundleUtils.isPreCompiledProductionBundle()
? Mode.PRODUCTION_PRECOMPILED_BUNDLE
: Mode.PRODUCTION_CUSTOM;
} else if (frontendHotdeploy()) {
return Mode.DEVELOPMENT_FRONTEND_LIVERELOAD;
} else {
Expand Down
Expand Up @@ -16,10 +16,6 @@

package com.vaadin.flow.server;

import static com.vaadin.flow.server.Constants.VAADIN_MAPPING;
import static com.vaadin.flow.server.frontend.FrontendUtils.EXPORT_CHUNK;
import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
Expand Down Expand Up @@ -88,6 +84,7 @@
import com.vaadin.flow.server.frontend.DevBundleUtils;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.server.frontend.ThemeUtils;
import com.vaadin.flow.server.startup.ApplicationConfiguration;
import com.vaadin.flow.shared.ApplicationConstants;
import com.vaadin.flow.shared.VaadinUriResolver;
import com.vaadin.flow.shared.communication.PushMode;
Expand All @@ -99,6 +96,9 @@
import elemental.json.JsonObject;
import elemental.json.JsonValue;
import elemental.json.impl.JsonUtil;
import static com.vaadin.flow.server.Constants.VAADIN_MAPPING;
import static com.vaadin.flow.server.frontend.FrontendUtils.EXPORT_CHUNK;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
* Request handler which handles bootstrapping of the application, i.e. the
Expand Down Expand Up @@ -1613,56 +1613,65 @@ protected static void addJavaScriptEntryPoints(
* (typically styles.css or document.css), which are served in express build
* mode by static file server directly from frontend/themes folder.
*
* @param config
* deployment configuration
* @param context
* the vaadin context
* @param fileName
* the stylesheet file name to add a reference to
* @return the collection of link tags to be added to the page
* @throws IOException
* if theme name cannot be extracted from file
*/
protected static Collection<Element> getStylesheetTags(
AbstractConfiguration config, String fileName) throws IOException {
return ThemeUtils.getActiveThemes(config).stream()
.map(theme -> getDevModeStyleTag(theme, fileName, config))
.toList();
VaadinContext context, String fileName) throws IOException {
ApplicationConfiguration config = ApplicationConfiguration.get(context);
return ThemeUtils.getActiveThemes(context).stream()
.map(theme -> getStyleTag(theme, fileName, config)).toList();
}

/**
* Gives a links for referencing the custom theme stylesheet files
* (typically styles.css or document.css), which are served in express build
* mode by static file server directly from frontend/themes folder.
*
* @param config
* deployment configuration
* @param context
* the vaadin context
* @param fileName
* the stylesheet file name to add a reference to
* @return the collection of links to be added to the page
* @throws IOException
* if theme name cannot be extracted from file
*/
protected static Collection<String> getStylesheetLinks(
AbstractConfiguration config, String fileName) throws IOException {
return ThemeUtils.getActiveThemes(config).stream()
VaadinContext context, String fileName) {
return ThemeUtils.getActiveThemes(context).stream()
.map(theme -> ThemeUtils.getThemeFilePath(theme, fileName))
.toList();
}

private static Element getDevModeStyleTag(String themeName, String fileName,
private static Element getStyleTag(String themeName, String fileName,
AbstractConfiguration config) {
Element element = new Element("style");
element.attr("data-file-path",
ThemeUtils.getThemeFilePath(themeName, fileName));
File frontendDirectory = FrontendUtils.getProjectFrontendDir(config);
File stylesCss = new File(
ThemeUtils.getThemeFolder(frontendDirectory, themeName),
fileName);
Element element;
try {
element.appendChild(new DataNode(CssBundler
.inlineImports(stylesCss.getParentFile(), stylesCss)));
String themeFilePath = ThemeUtils.getThemeFilePath(themeName,
fileName);
if (config.isProductionMode()) {
element = new Element("link");
element.attr("rel", "stylesheet");
element.attr("type", "text/css");
element.attr("href", themeFilePath);
} else {
element = new Element("style");
element.attr("data-file-path", themeFilePath);
File frontendDirectory = FrontendUtils
.getProjectFrontendDir(config);
File stylesCss = new File(
ThemeUtils.getThemeFolder(frontendDirectory, themeName),
fileName);
// Inline CSS into style tag to have hot module reload feature
element.appendChild(new DataNode(CssBundler
.inlineImports(stylesCss.getParentFile(), stylesCss)));
}
} catch (IOException e) {
throw new RuntimeException(
"Unable to read theme file from " + stylesCss, e);
"Unable to read theme file from " + fileName, e);
}
return element;
}
Expand Down
13 changes: 10 additions & 3 deletions flow-server/src/main/java/com/vaadin/flow/server/Mode.java
Expand Up @@ -21,13 +21,20 @@
* One of production, development using livereload or development using bundle
*/
public enum Mode {
PRODUCTION("production"), DEVELOPMENT_FRONTEND_LIVERELOAD(
"development"), DEVELOPMENT_BUNDLE("development");
PRODUCTION_CUSTOM("production", true), PRODUCTION_PRECOMPILED_BUNDLE(
"production", true), DEVELOPMENT_FRONTEND_LIVERELOAD("development",
false), DEVELOPMENT_BUNDLE("development", false);

private final String name;
private final boolean production;

Mode(String name) {
Mode(String name, boolean production) {
this.name = name;
this.production = production;
}

public boolean isProduction() {
return production;
}

@Override
Expand Down
Expand Up @@ -46,6 +46,7 @@
import com.vaadin.flow.internal.ResponseWriter;
import com.vaadin.flow.server.frontend.DevBundleUtils;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.server.frontend.ThemeUtils;

import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -276,6 +277,13 @@ public boolean serveStaticResource(HttpServletRequest request,
deploymentConfiguration.getProjectFolder(),
filenameWithPath.replace(VAADIN_MAPPING, ""));
}
} else if (deploymentConfiguration
.getMode() == Mode.PRODUCTION_PRECOMPILED_BUNDLE
&& APP_THEME_PATTERN.matcher(filenameWithPath).find()) {
resourceUrl = ThemeUtils
.getThemeResourceFromPrecompiledProductionBundle(
filenameWithPath.replace(VAADIN_MAPPING, "")
.replaceFirst("^/", ""));
} else if (APP_THEME_ASSETS_PATTERN.matcher(filenameWithPath).find()) {
resourceUrl = vaadinService.getClassLoader()
.getResource(VAADIN_WEBAPP_RESOURCES + "VAADIN/static/"
Expand Down
Expand Up @@ -48,6 +48,7 @@
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinServletContext;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.frontend.BundleUtils;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.server.frontend.ThemeUtils;
import com.vaadin.flow.server.startup.ApplicationConfiguration;
Expand Down Expand Up @@ -182,9 +183,10 @@ public boolean synchronizedHandleRequest(VaadinSession session,
private static void addDevBundleTheme(Document document,
VaadinContext context) {
ApplicationConfiguration config = ApplicationConfiguration.get(context);
if (config.getMode() == Mode.DEVELOPMENT_BUNDLE) {
if (config.getMode() == Mode.DEVELOPMENT_BUNDLE
|| (config.getMode() == Mode.PRODUCTION_PRECOMPILED_BUNDLE)) {
try {
BootstrapHandler.getStylesheetTags(config, "styles.css")
BootstrapHandler.getStylesheetTags(context, "styles.css")
.forEach(link -> document.head().appendChild(link));
} catch (IOException e) {
throw new UncheckedIOException(
Expand Down Expand Up @@ -408,7 +410,7 @@ private static Document getIndexHtmlDocument(VaadinService service)

Document indexHtmlDocument = Jsoup.parse(index);
Mode mode = config.getMode();
if (mode == Mode.PRODUCTION) {
if (mode.isProduction()) {
// The index.html is fetched from the bundle so it includes the
// entry point javascripts
} else if (mode == Mode.DEVELOPMENT_BUNDLE) {
Expand Down
Expand Up @@ -52,11 +52,13 @@
import com.vaadin.flow.server.HandlerHelper;
import com.vaadin.flow.server.Mode;
import com.vaadin.flow.server.PwaRegistry;
import com.vaadin.flow.server.VaadinContext;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinResponse;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinServletRequest;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.frontend.BundleUtils;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.server.webcomponent.WebComponentConfigurationRegistry;
import com.vaadin.flow.shared.ApplicationConstants;
Expand Down Expand Up @@ -388,18 +390,22 @@ protected void writeBootstrapPage(String contentType,

DeploymentConfiguration config = response.getService()
.getDeploymentConfiguration();
VaadinContext context = response.getService().getContext();

// stylesheet tags are only added in dev mode, because Flow always
// rebuilds prod bundle whenever it spots embedded web components, so
// all the styles are included into the custom bundle
if (config.getMode() == Mode.DEVELOPMENT_BUNDLE) {
// Add styles.css link to the web component shadow DOM
BootstrapHandler.getStylesheetTags(config, "styles.css")
BootstrapHandler.getStylesheetTags(context, "styles.css")
.forEach(element -> ElementUtil.fromJsoup(element)
.ifPresent(elementsForShadows::add));

// Add document.css link to the document
BootstrapHandler.getStylesheetLinks(config, "document.css")
BootstrapHandler.getStylesheetLinks(context, "document.css")
.forEach(link -> UI.getCurrent().getPage().executeJs(
BootstrapHandler.SCRIPT_TEMPLATE_FOR_STYLESHEET_LINK_TAG,
link));

}

WebComponentConfigurationRegistry
Expand Down
Expand Up @@ -113,6 +113,18 @@ public static String getChunkId(String className) {
return StringUtil.getHash(className, StandardCharsets.UTF_8);
}

/**
* Returns whether the application uses pre-compiled production bundle or a
* custom bundle.
*
* @return <code>true</code> in case of pre-compiled bundle,
* <code>false</code> otherwise
*/
public static boolean isPreCompiledProductionBundle() {
JsonObject stats = loadStatsJson();
return stats.hasKey("pre-compiled");
}

private static Logger getLogger() {
return LoggerFactory.getLogger(BundleUtils.class);
}
Expand Down
Expand Up @@ -71,7 +71,7 @@ public static boolean needsBuild(Options options,
getLogger().info("Checking if a {} mode bundle build is needed", mode);
try {
boolean needsBuild;
if (Mode.PRODUCTION == mode) {
if (mode.isProduction()) {
if (options.isForceProductionBuild()
|| EndpointRequestUtil.isHillaAvailable()) {
getLogger().info("Frontend build requested.");
Expand Down Expand Up @@ -189,7 +189,7 @@ private static boolean needsBuildInternal(Options options,
}

if (ThemeValidationUtil.themeConfigurationChanged(options, statsJson,
frontendDependencies)) {
frontendDependencies, finder)) {
UsageStatistics.markAsUsed(
"flow/rebundle-reason-changed-theme-config", null);
return true;
Expand Down Expand Up @@ -373,7 +373,7 @@ public static boolean hashAndBundleModulesEqual(JsonObject statsJson,

if (bundleModules == null) {
getLogger().error(
"Dev bundle did not contain package json dependencies to validate.\n"
"Bundle did not contain package json dependencies to validate.\n"
+ "Rebuild of bundle needed.");
return false;
}
Expand Down Expand Up @@ -492,7 +492,7 @@ public static boolean exportedWebComponents(JsonObject statsJson,
if (!webComponents.isEmpty()) {
getLogger().info(
"Found embedded web components not yet included "
+ "into the dev bundle: {}",
+ "into the bundle: {}",
String.join(", ", webComponents));
return true;
}
Expand All @@ -509,7 +509,7 @@ public static boolean exportedWebComponents(JsonObject statsJson,
if (!webComponents.isEmpty()) {
getLogger().info(
"Found newly added embedded web components not "
+ "yet included into the dev bundle: {}",
+ "yet included into the bundle: {}",
String.join(", ", webComponents));
return true;
}
Expand Down
Expand Up @@ -76,7 +76,7 @@ public class NodeTasks implements FallibleCommand {
TaskUpdateThemeImport.class,
TaskCopyTemplateFiles.class,
TaskRunDevBundleBuild.class,
TaskCopyBundleFiles.class
TaskPrepareProdBundle.class
));
// @formatter:on

Expand Down Expand Up @@ -111,11 +111,12 @@ public NodeTasks(Options options) {

if (options.isProductionMode()) {
boolean needBuild = BundleValidationUtil.needsBuild(options,
frontendDependencies, classFinder, Mode.PRODUCTION);
frontendDependencies, classFinder,
Mode.PRODUCTION_PRECOMPILED_BUNDLE);
options.withRunNpmInstall(needBuild);
options.withBundleBuild(needBuild);
if (!needBuild) {
commands.add(new TaskCopyBundleFiles(options));
commands.add(new TaskPrepareProdBundle(options));
}
} else if (options.isBundleBuild()) {
// The dev bundle check needs the frontendDependencies to be
Expand Down

0 comments on commit 281e90b

Please sign in to comment.