Skip to content

Commit

Permalink
fix: Watch component folders in parent themes (#10834) (#10883)
Browse files Browse the repository at this point in the history
Adds parent themes component folders to be watched by webpack.

Related-to: #9948
  • Loading branch information
vaadin-bot committed May 3, 2021
1 parent 2865948 commit 91c4aa7
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 51 deletions.
Expand Up @@ -14,7 +14,7 @@
* the License.
*/

const { processThemeResources, extractThemeName } = require('./theme-handle');
const { processThemeResources, extractThemeName, findParentThemes } = require('./theme-handle');

/**
* The application theme plugin is for generating, collecting and copying of theme files for the application theme.
Expand Down Expand Up @@ -55,5 +55,5 @@ class ApplicationThemePlugin {

}

module.exports = { ApplicationThemePlugin, processThemeResources, extractThemeName };
module.exports = { ApplicationThemePlugin, processThemeResources, extractThemeName, findParentThemes };

Expand Up @@ -6,7 +6,7 @@
],
"repository": "vaadin/flow",
"name": "@vaadin/application-theme-plugin",
"version": "0.3.3",
"version": "0.4.0",
"main": "application-theme-plugin.js",
"author": "Vaadin Ltd",
"license": "Apache-2.0",
Expand Down
Expand Up @@ -166,4 +166,43 @@ function extractThemeName(frontendGeneratedFolder) {
}
}

module.exports = { processThemeResources, extractThemeName };
/**
* Finds all the parent themes located in the project themes folders with
* respect to the given custom theme with {@code themeName}.
* @param {string} themeName given custom theme name to look parents for
* @param {object} options application theme plugin mandatory options,
* @see {@link ApplicationThemePlugin}
* @returns {string[]} array of paths to found parent themes with respect to the
* given custom theme
*/
function findParentThemes(themeName, options) {
const existingThemeFolders = options.themeProjectFolders.filter(
(folder) => fs.existsSync(folder));
return collectParentThemes(themeName, existingThemeFolders, false);
}

function collectParentThemes(themeName, themeFolders, isParent) {
let foundParentThemes = [];
themeFolders.forEach(folder => {
const themeFolder = path.resolve(folder, themeName);
if (fs.existsSync(themeFolder)) {
const themeProperties = getThemeProperties(themeFolder);

if (themeProperties.parent) {
foundParentThemes.push(...collectParentThemes(themeProperties.parent, themeFolders, true));
if (!foundParentThemes.length) {
throw new Error("Could not locate files for defined parent theme '" + themeProperties.parent + "'.\n" +
"Please verify that dependency is added or theme folder exists.")
}
}
// Add a theme path to result collection only if a given themeName
// is supposed to be a parent theme
if (isParent) {
foundParentThemes.push(themeFolder);
}
}
});
return foundParentThemes;
}

module.exports = { processThemeResources, extractThemeName, findParentThemes };
32 changes: 19 additions & 13 deletions flow-server/src/main/resources/webpack.generated.js
Expand Up @@ -16,7 +16,7 @@ const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
// Flow plugins
const StatsPlugin = require('@vaadin/stats-plugin');
const ThemeLiveReloadPlugin = require('@vaadin/theme-live-reload-plugin');
const { ApplicationThemePlugin, processThemeResources, extractThemeName } = require('@vaadin/application-theme-plugin');
const { ApplicationThemePlugin, processThemeResources, extractThemeName, findParentThemes } = require('@vaadin/application-theme-plugin');

const path = require('path');

Expand Down Expand Up @@ -176,12 +176,6 @@ if (devMode) {
}

const flowFrontendThemesFolder = path.resolve(flowFrontendFolder, 'themes');
let themeName = undefined;
if (devMode) {
// Current theme name is being extracted from theme.js located in frontend
// generated folder
themeName = extractThemeName(frontendGeneratedFolder);
}
const themeOptions = {
devMode: devMode,
// The following matches folder 'target/flow-frontend/themes/'
Expand All @@ -191,6 +185,23 @@ const themeOptions = {
projectStaticAssetsOutputFolder: projectStaticAssetsOutputFolder,
frontendGeneratedFolder: frontendGeneratedFolder
};
let themeName = undefined;
let themeWatchFolders = undefined;
if (devMode) {
// Current theme name is being extracted from theme.js located in frontend
// generated folder
themeName = extractThemeName(frontendGeneratedFolder);
const parentThemePaths = findParentThemes(themeName, themeOptions);
const currentThemeFolders = projectStaticAssetsFolders
.map((folder) => path.resolve(folder, "themes", themeName));
// Watch the components folders for component styles update in both
// current theme and parent themes. Other folders or CSS files except
// 'styles.css' should be referenced from `styles.css` anyway, so no need
// to watch them.
themeWatchFolders = [...currentThemeFolders, ...parentThemePaths]
.map((themeFolder) => path.resolve(themeFolder, "components"));
}

const processThemeResourcesCallback = (logger) => processThemeResources(themeOptions, logger);

exports = {
Expand Down Expand Up @@ -321,12 +332,7 @@ module.exports = {

...(devMode && themeName ? [new ExtraWatchWebpackPlugin({
files: [],
// Watch the components folder for component styles update.
// Other folders or CSS files except 'styles.css' should be
// referenced from `styles.css` anyway, so no need to watch them.
dirs: [path.resolve(__dirname, 'frontend', 'themes', themeName, 'components'),
path.resolve(__dirname, 'src', 'main', 'resources', 'META-INF', 'resources', 'themes', themeName, 'components'),
path.resolve(__dirname, 'src', 'main', 'resources', 'static', 'themes', themeName, 'components')]
dirs: [...themeWatchFolders]
}), new ThemeLiveReloadPlugin(processThemeResourcesCallback)] : []),

new StatsPlugin({
Expand Down
@@ -1 +1 @@
Empty file here to ensure the components folder is created
Empty file here to ensure the components folder is created
@@ -0,0 +1,3 @@
{
"parent": "parent-theme"
}
@@ -0,0 +1 @@
Empty file here to ensure the components folder is created
Expand Up @@ -46,28 +46,57 @@ public class ComponentThemeLiveReloadIT extends ChromeBrowserTest {

private static final String BORDER_RADIUS = "3px";
private static final String OTHER_BORDER_RADIUS = "6px";
private static final String THEME_FOLDER = "frontend/themes/app-theme/";
private static final String PARENT_BORDER_RADIUS = "9px";

private File componentCSSFile;
private File themeGeneratedFile;
private static final String THEMES_FOLDER = "frontend/themes/";
private static final String CURRENT_THEME = "app-theme";
private static final String PARENT_THEME = "parent-theme";
private static final String CURRENT_THEME_FOLDER = THEMES_FOLDER
+ CURRENT_THEME + "/";
private static final String PARENT_THEME_FOLDER = THEMES_FOLDER
+ PARENT_THEME + "/";
private static final String THEME_GENERATED_PATTERN = "frontend/generated/theme-%s.generated.js";
private static final String COMPONENT_STYLE_SHEET = "components/vaadin-text-field.css";

private File currentThemeComponentCSSFile;
private File currentThemeGeneratedFile;

private File parentThemeComponentCSSFile;
private File parentThemeGeneratedFile;

@Before
public void init() {
File baseDir = new File(System.getProperty("user.dir", "."));
final File themeFolder = new File(baseDir, THEME_FOLDER);
componentCSSFile = new File(new File(themeFolder, "components"),
"vaadin-text-field.css");
themeGeneratedFile = new File(baseDir,
"frontend/generated/theme-app-theme.generated.js");

final File currentThemeFolder = new File(baseDir, CURRENT_THEME_FOLDER);
currentThemeComponentCSSFile = new File(currentThemeFolder,
COMPONENT_STYLE_SHEET);
currentThemeGeneratedFile = new File(baseDir,
String.format(THEME_GENERATED_PATTERN, CURRENT_THEME));

final File parentThemeFolder = new File(baseDir, PARENT_THEME_FOLDER);
parentThemeComponentCSSFile = new File(parentThemeFolder,
COMPONENT_STYLE_SHEET);
parentThemeGeneratedFile = new File(baseDir,
String.format(THEME_GENERATED_PATTERN, PARENT_THEME));
}

@After
public void cleanUp() {
if (componentCSSFile.exists()) {
if (currentThemeComponentCSSFile.exists()) {
// This waits until live reload complete to not affect the second
// re-run in CI (if any) and to not affect other @Test methods
// (if any appear in the future)
doActionAndWaitUntilLiveReloadComplete(this::deleteComponentStyles);
doActionAndWaitUntilLiveReloadComplete(
this::deleteCurrentThemeComponentStyles);
}

if (parentThemeComponentCSSFile.exists()) {
// This waits until live reload complete to not affect the second
// re-run in CI (if any) and to not affect other @Test methods
// (if any appear in the future)
doActionAndWaitUntilLiveReloadComplete(
this::deleteParentThemeComponentStyles);
}
}

Expand All @@ -80,27 +109,49 @@ public void webpackLiveReload_newComponentStylesCreatedAndDeleted_stylesUpdatedO
isComponentCustomStyle(BORDER_RADIUS)
|| isComponentCustomStyle(OTHER_BORDER_RADIUS));

// Test current theme live reload:

// Live reload upon adding a new component styles file
doActionAndWaitUntilLiveReloadComplete(
() -> createOrUpdateComponentCSSFile(BORDER_RADIUS));
() -> createOrUpdateComponentCSSFile(BORDER_RADIUS,
currentThemeComponentCSSFile));
waitUntilComponentCustomStyle(BORDER_RADIUS);

// Live reload upon updating component styles file
doActionAndWaitUntilLiveReloadComplete(
() -> createOrUpdateComponentCSSFile(OTHER_BORDER_RADIUS));
() -> createOrUpdateComponentCSSFile(OTHER_BORDER_RADIUS,
currentThemeComponentCSSFile));
waitUntilComponentCustomStyle(OTHER_BORDER_RADIUS);

// Live reload upon file deletion
doActionAndWaitUntilLiveReloadComplete(this::deleteComponentStyles);
waitUntilComponentInitialStyle();
checkNoWebpackErrors();
doActionAndWaitUntilLiveReloadComplete(
this::deleteCurrentThemeComponentStyles);
waitUntilComponentInitialStyle(
"Wait for current theme component initial styles timeout");
checkNoWebpackErrors(CURRENT_THEME);

// Test parent theme live reload:

// Live reload upon adding a new component styles file to parent theme
doActionAndWaitUntilLiveReloadComplete(
() -> createOrUpdateComponentCSSFile(PARENT_BORDER_RADIUS,
parentThemeComponentCSSFile));
waitUntilComponentCustomStyle(PARENT_BORDER_RADIUS);

// Live reload upon parent theme file deletion
doActionAndWaitUntilLiveReloadComplete(
this::deleteParentThemeComponentStyles);
waitUntilComponentInitialStyle(
"Wait for parent theme component initial styles timeout");
checkNoWebpackErrors(PARENT_THEME);
}

private void waitUntilComponentInitialStyle() {
private void waitUntilComponentInitialStyle(String errMessage) {
waitUntilWithMessage(
driver -> !isComponentCustomStyle(BORDER_RADIUS)
&& !isComponentCustomStyle(OTHER_BORDER_RADIUS),
"Wait for component initial styles timeout");
&& !isComponentCustomStyle(OTHER_BORDER_RADIUS)
&& !isComponentCustomStyle(PARENT_BORDER_RADIUS),
errMessage);
}

private void waitUntilComponentCustomStyle(String borderRadius) {
Expand All @@ -122,29 +173,41 @@ private boolean isComponentCustomStyle(String borderRadius) {
}
}

private void createOrUpdateComponentCSSFile(String borderRadius) {
private void createOrUpdateComponentCSSFile(String borderRadius,
File componentCssFile) {
try {
// @formatter:off
final String componentStyles =
"[part=\"input-field\"] {\n" +
" border-radius: " + borderRadius + ";\n" +
"}";
// @formatter:on
FileUtils.write(componentCSSFile, componentStyles,
FileUtils.write(componentCssFile, componentStyles,
StandardCharsets.UTF_8.name());
waitUntil(driver -> componentCSSFile.exists());
waitUntil(driver -> componentCssFile.exists());
} catch (IOException e) {
throw new RuntimeException("Failed to apply component styles", e);
throw new RuntimeException(
"Failed to apply component styles in " + componentCssFile,
e);
}
}

private void deleteComponentStyles() {
private void deleteCurrentThemeComponentStyles() {
Assert.assertTrue("Expected theme generated file to be present",
themeGeneratedFile.exists());
currentThemeGeneratedFile.exists());
// workaround for https://github.com/vaadin/flow/issues/9948
// delete theme generated with component styles in one run
deleteFile(themeGeneratedFile);
deleteFile(componentCSSFile);
deleteFile(currentThemeGeneratedFile);
deleteFile(currentThemeComponentCSSFile);
}

private void deleteParentThemeComponentStyles() {
Assert.assertTrue("Expected parent theme generated file to be present",
parentThemeGeneratedFile.exists());
// workaround for https://github.com/vaadin/flow/issues/9948
// delete theme generated with component styles in one run
deleteFile(parentThemeGeneratedFile);
deleteFile(parentThemeComponentCSSFile);
}

private void deleteFile(File fileToDelete) {
Expand Down Expand Up @@ -202,13 +265,13 @@ private void waitUntilWithMessage(ExpectedCondition<?> condition,
}
}

private void checkNoWebpackErrors() {
private void checkNoWebpackErrors(String theme) {
getLogEntries(Level.ALL).forEach(logEntry -> {
if (logEntry.getMessage().contains("Module build failed")) {
Assert.fail(
Assert.fail(String.format(
"Webpack error detected in the browser console after "
+ "deleting component style sheet:\n\n"
+ logEntry.getMessage());
+ "deleting '%s' component style sheet: %s\n\n",
theme, logEntry.getMessage()));
}
});

Expand All @@ -217,9 +280,10 @@ private void checkNoWebpackErrors() {
waitForElementNotPresent(byErrorOverlayClass);
} catch (TimeoutException e) {
WebElement error = findElement(byErrorOverlayClass);
Assert.fail(
"Webpack error overlay detected after deleting component "
+ "style sheet:\n\n" + error.getText());
Assert.fail(String.format(
"Webpack error overlay detected after deleting '%s' "
+ "component style sheet: %s\n\n",
theme, error.getText()));
}
}
}

0 comments on commit 91c4aa7

Please sign in to comment.