Skip to content
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

fix: Watch component folders in parent themes (#10834) (CP: 6.0) #10883

Merged
merged 1 commit into from
May 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
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 };

Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Empty file here to ensure the components folder is created
Empty file here to ensure the components folder is created
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"parent": "parent-theme"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Empty file here to ensure the components folder is created
Original file line number Diff line number Diff line change
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()));
}
}
}