diff --git a/flow-server/src/main/resources/plugins/application-theme-plugin/application-theme-plugin.js b/flow-server/src/main/resources/plugins/application-theme-plugin/application-theme-plugin.js index 074227d0b5a..a4926aa8072 100644 --- a/flow-server/src/main/resources/plugins/application-theme-plugin/application-theme-plugin.js +++ b/flow-server/src/main/resources/plugins/application-theme-plugin/application-theme-plugin.js @@ -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. @@ -55,5 +55,5 @@ class ApplicationThemePlugin { } -module.exports = { ApplicationThemePlugin, processThemeResources, extractThemeName }; +module.exports = { ApplicationThemePlugin, processThemeResources, extractThemeName, findParentThemes }; diff --git a/flow-server/src/main/resources/plugins/application-theme-plugin/package.json b/flow-server/src/main/resources/plugins/application-theme-plugin/package.json index 905d249d9c8..9cd8f181d1a 100644 --- a/flow-server/src/main/resources/plugins/application-theme-plugin/package.json +++ b/flow-server/src/main/resources/plugins/application-theme-plugin/package.json @@ -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", diff --git a/flow-server/src/main/resources/plugins/application-theme-plugin/theme-handle.js b/flow-server/src/main/resources/plugins/application-theme-plugin/theme-handle.js index 797b1fb12c8..78d7fb36bfd 100644 --- a/flow-server/src/main/resources/plugins/application-theme-plugin/theme-handle.js +++ b/flow-server/src/main/resources/plugins/application-theme-plugin/theme-handle.js @@ -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 }; diff --git a/flow-server/src/main/resources/webpack.generated.js b/flow-server/src/main/resources/webpack.generated.js index 3dfb8c46b79..4ab54ad2ed2 100644 --- a/flow-server/src/main/resources/webpack.generated.js +++ b/flow-server/src/main/resources/webpack.generated.js @@ -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'); @@ -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/' @@ -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 = { @@ -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({ diff --git a/flow-tests/test-application-theme/test-theme-component-live-reload/frontend/themes/app-theme/components/empty b/flow-tests/test-application-theme/test-theme-component-live-reload/frontend/themes/app-theme/components/empty index 7a23df59fa4..7398a722122 100644 --- a/flow-tests/test-application-theme/test-theme-component-live-reload/frontend/themes/app-theme/components/empty +++ b/flow-tests/test-application-theme/test-theme-component-live-reload/frontend/themes/app-theme/components/empty @@ -1 +1 @@ -Empty file here to ensure the components folder is created \ No newline at end of file +Empty file here to ensure the components folder is created diff --git a/flow-tests/test-application-theme/test-theme-component-live-reload/frontend/themes/app-theme/theme.json b/flow-tests/test-application-theme/test-theme-component-live-reload/frontend/themes/app-theme/theme.json new file mode 100644 index 00000000000..4d8f1f3e8a6 --- /dev/null +++ b/flow-tests/test-application-theme/test-theme-component-live-reload/frontend/themes/app-theme/theme.json @@ -0,0 +1,3 @@ +{ + "parent": "parent-theme" +} diff --git a/flow-tests/test-application-theme/test-theme-component-live-reload/frontend/themes/parent-theme/components/empty b/flow-tests/test-application-theme/test-theme-component-live-reload/frontend/themes/parent-theme/components/empty new file mode 100644 index 00000000000..7398a722122 --- /dev/null +++ b/flow-tests/test-application-theme/test-theme-component-live-reload/frontend/themes/parent-theme/components/empty @@ -0,0 +1 @@ +Empty file here to ensure the components folder is created diff --git a/flow-tests/test-application-theme/test-theme-component-live-reload/frontend/themes/parent-theme/styles.css b/flow-tests/test-application-theme/test-theme-component-live-reload/frontend/themes/parent-theme/styles.css new file mode 100644 index 00000000000..e69de29bb2d diff --git a/flow-tests/test-application-theme/test-theme-component-live-reload/src/test/java/com/vaadin/flow/uitest/ui/ComponentThemeLiveReloadIT.java b/flow-tests/test-application-theme/test-theme-component-live-reload/src/test/java/com/vaadin/flow/uitest/ui/ComponentThemeLiveReloadIT.java index bce4726b8c0..4f8523101c6 100644 --- a/flow-tests/test-application-theme/test-theme-component-live-reload/src/test/java/com/vaadin/flow/uitest/ui/ComponentThemeLiveReloadIT.java +++ b/flow-tests/test-application-theme/test-theme-component-live-reload/src/test/java/com/vaadin/flow/uitest/ui/ComponentThemeLiveReloadIT.java @@ -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); } } @@ -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) { @@ -122,7 +173,8 @@ private boolean isComponentCustomStyle(String borderRadius) { } } - private void createOrUpdateComponentCSSFile(String borderRadius) { + private void createOrUpdateComponentCSSFile(String borderRadius, + File componentCssFile) { try { // @formatter:off final String componentStyles = @@ -130,21 +182,32 @@ private void createOrUpdateComponentCSSFile(String borderRadius) { " 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) { @@ -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())); } }); @@ -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())); } } }