Skip to content

Commit

Permalink
fix: recompile Webpack when adding a new CSS file to app theme (#9708)
Browse files Browse the repository at this point in the history
Adding a new file to frontend/themes/my-theme now triggers Webpack recompilation and updates the application.

Fixes: #9594
# Conflicts:
#	flow-server/src/main/resources/webpack.generated.js
  • Loading branch information
mshabarov authored and pleku committed Mar 10, 2021
1 parent eb436c5 commit b266396
Show file tree
Hide file tree
Showing 16 changed files with 599 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ static Map<String, String> getDefaultDevDependencies() {
defaults.put("webpack-babel-multi-target-plugin", "2.3.3");
defaults.put("copy-webpack-plugin", "5.1.2");
defaults.put("compression-webpack-plugin", "4.0.1");
defaults.put("extra-watch-webpack-plugin", "1.0.3");
defaults.put("webpack-merge", "4.2.2");
defaults.put("raw-loader", "3.1.0");
defaults.put("css-loader", "4.2.1");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,7 @@
* the License.
*/

const fs = require('fs');
const path = require('path');
const generateThemeFile = require('./theme-generator');
const {copyStaticAssets} = require('./theme-copy');

let logger;
let executionOptions;

// matches theme folder name in 'themes/my-theme/my-theme.js'
const nameRegex = /themes\/(.*)\/\1.generated.js/g;
const { processThemeResources, extractThemeName } = require('./theme-handle');

/**
* The application theme plugin is for generating, collecting and copying of theme files for the application theme.
Expand Down Expand Up @@ -52,112 +43,13 @@ class ApplicationThemePlugin {
}

apply(compiler) {
logger = compiler.getInfrastructureLogger("ApplicationThemePlugin");

compiler.hooks.afterEnvironment.tap("ApplicationThemePlugin", () => {
const generatedThemeFile = path.resolve(this.options.themeResourceFolder, "theme-generated.js");
if (fs.existsSync(generatedThemeFile)) {

// read theme name from the theme-generated.js as there we always mark the used theme for webpack to handle.
const themeName = nameRegex.exec(fs.readFileSync(generatedThemeFile, {encoding: 'utf8'}))[1];
if (!themeName) {
throw new Error("Couldn't parse theme name from '" + generatedThemeFile + "'.");
}
executionOptions = this.options;
findThemeFolderAndHandleTheme(themeName);
const logger = compiler.getInfrastructureLogger("ApplicationThemePlugin");

} else {
logger.debug("Skipping Vaadin application theme handling.");
logger.trace("Most likely no @Theme annotation for application or only themeClass used.");
}
});
compiler.hooks.afterEnvironment.tap("ApplicationThemePlugin",
() => processThemeResources(this.options, logger));
}
}

module.exports = ApplicationThemePlugin;

/**
* Search for the given theme in the project and resource folders.
*
* @param {string} name of theme to find
*
* @return true or false for if theme was found
*/
function findThemeFolderAndHandleTheme(themeName) {

let themeFound = false;
for (let i = 0; i < executionOptions.themeProjectFolders.length; i++) {
const themeProjectFolder = executionOptions.themeProjectFolders[i];
if (fs.existsSync(themeProjectFolder)) {
logger.info("Searching themes folder", themeProjectFolder, "for theme", themeName);
const handled = handleThemes(themeName, themeProjectFolder, executionOptions.projectStaticAssetsOutputFolder);
if (handled) {
if (themeFound) {
throw new Error("Found theme files in '" + themeProjectFolder + "' and '"
+ themeFound + "'. Theme should only be available in one folder");
}
logger.info("Found theme files from '" + themeProjectFolder + "'");
themeFound = themeProjectFolder;
}
}
}

if (fs.existsSync(executionOptions.themeResourceFolder)) {
if (themeFound && fs.existsSync(path.resolve(executionOptions.themeResourceFolder, themeName))) {
throw new Error("Theme '" + themeName + "'should not exist inside a jar and in the project at the same time\n" +
"Extending another theme is possible by adding { \"parent\": \"my-parent-theme\" } entry to the theme.json file inside your theme folder.");
}
logger.debug("Searching theme jar resource folder ", executionOptions.themeResourceFolder, " for theme ", themeName);
handleThemes(themeName, executionOptions.themeResourceFolder, executionOptions.projectStaticAssetsOutputFolder);
themeFound = true;
}
return themeFound;
}

/**
* Copies static resources for theme and generates/writes the [theme-name].js for webpack to handle.
*
* Note! If a parent theme is defined it will also be handled here so that the parent theme generated file is
* generated in advance of the theme generated file.
*
* @param {string} themeName name of theme to handle
* @param {string} themesFolder folder containing application theme folders
* @param {string} projectStaticAssetsOutputFolder folder to output files to
*
* @throws Error if parent theme defined, but can't locate parent theme
*
* @returns true if theme was found else false.
*/
function handleThemes(themeName, themesFolder, projectStaticAssetsOutputFolder) {
const themeFolder = path.resolve(themesFolder, themeName);
if (fs.existsSync(themeFolder)) {
logger.debug("Found theme ", themeName, " in folder ", themeFolder);

const themeProperties = getThemeProperties(themeFolder);
module.exports = { ApplicationThemePlugin, processThemeResources, extractThemeName };

// If theme has parent handle parent theme immediately.
if (themeProperties.parent) {
const found = findThemeFolderAndHandleTheme(themeProperties.parent);
if (!found) {
throw new Error("Could not locate files for defined parent theme '" + themeProperties.parent + "'.\n" +
"Please verify that dependency is added or theme folder exists.")
}
}

copyStaticAssets(themeName, themeProperties, projectStaticAssetsOutputFolder, logger);

const themeFile = generateThemeFile(themeFolder, themeName, themeProperties);

fs.writeFileSync(path.resolve(themeFolder, themeName + '.generated.js'), themeFile);
return true;
}
return false;
};

function getThemeProperties(themeFolder) {
const themePropertyFile = path.resolve(themeFolder, 'theme.json');
if (!fs.existsSync(themePropertyFile)) {
return {};
}
return JSON.parse(fs.readFileSync(themePropertyFile));
};
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.2.7",
"version": "0.2.8",
"main": "application-theme-plugin.js",
"author": "Vaadin Ltd",
"license": "Apache-2.0",
Expand All @@ -16,6 +16,7 @@
"files": [
"application-theme-plugin.js",
"theme-generator.js",
"theme-copy.js"
"theme-copy.js",
"theme-handle.js"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Copyright 2000-2020 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

/**
* This file contains functions for look up and handle the theme resources
* for application theme plugin.
*/
const fs = require('fs');
const path = require('path');
const generateThemeFile = require('./theme-generator');
const {copyStaticAssets} = require('./theme-copy');

// matches theme folder name in 'themes/my-theme/my-theme.generated.js'
const nameRegex = /themes\/(.*)\/\1.generated.js/;

/**
* Looks up for a theme resources in a current project and in jar dependencies,
* copies the found resources and generates/updates meta data for webpack
* compilation.
*
* @param {object} options application theme plugin mandatory options,
* @see {@link ApplicationThemePlugin}
*
* @param logger application theme plugin logger
*/
function processThemeResources(options, logger) {
const themeName = extractThemeName(options.themeResourceFolder);
if (themeName) {
findThemeFolderAndHandleTheme(themeName, options, logger);
} else {
logger.debug("Skipping Vaadin application theme handling.");
logger.trace("Most likely no @Theme annotation for application or only themeClass used.");
}
}

/**
* Search for the given theme in the project and resource folders.
*
* @param {string} themeName name of theme to find
* @param {object} options application theme plugin mandatory options,
* @see {@link ApplicationThemePlugin}
* @param logger application theme plugin logger
* @return true or false for if theme was found
*/
function findThemeFolderAndHandleTheme(themeName, options, logger) {
let themeFound = false;
for (let i = 0; i < options.themeProjectFolders.length; i++) {
const themeProjectFolder = options.themeProjectFolders[i];
if (fs.existsSync(themeProjectFolder)) {
logger.info("Searching themes folder", themeProjectFolder, "for theme", themeName);
const handled = handleThemes(themeName, themeProjectFolder, options, logger);
if (handled) {
if (themeFound) {
throw new Error("Found theme files in '" + themeProjectFolder + "' and '"
+ themeFound + "'. Theme should only be available in one folder");
}
logger.info("Found theme files from '" + themeProjectFolder + "'");
themeFound = themeProjectFolder;
}
}
}

if (fs.existsSync(options.themeResourceFolder)) {
if (themeFound && fs.existsSync(path.resolve(options.themeResourceFolder, themeName))) {
throw new Error("Theme '" + themeName + "'should not exist inside a jar and in the project at the same time\n" +
"Extending another theme is possible by adding { \"parent\": \"my-parent-theme\" } entry to the theme.json file inside your theme folder.");
}
logger.debug("Searching theme jar resource folder ", options.themeResourceFolder, " for theme ", themeName);
handleThemes(themeName, options.themeResourceFolder, options, logger);
themeFound = true;
}
return themeFound;
}

/**
* Copies static resources for theme and generates/writes the
* [theme-name].generated.js for webpack to handle.
*
* Note! If a parent theme is defined it will also be handled here so that the parent theme generated file is
* generated in advance of the theme generated file.
*
* @param {string} themeName name of theme to handle
* @param {string} themesFolder folder containing application theme folders
* @param {object} options application theme plugin mandatory options,
* @see {@link ApplicationThemePlugin}
* @param {object} logger plugin logger instance
*
* @throws Error if parent theme defined, but can't locate parent theme
*
* @returns true if theme was found else false.
*/
function handleThemes(themeName, themesFolder, options, logger) {
const themeFolder = path.resolve(themesFolder, themeName);
if (fs.existsSync(themeFolder)) {
logger.debug("Found theme ", themeName, " in folder ", themeFolder);

const themeProperties = getThemeProperties(themeFolder);

// If theme has parent handle parent theme immediately.
if (themeProperties.parent) {
const found = findThemeFolderAndHandleTheme(themeProperties.parent, options, logger);
if (!found) {
throw new Error("Could not locate files for defined parent theme '" + themeProperties.parent + "'.\n" +
"Please verify that dependency is added or theme folder exists.")
}
}

copyStaticAssets(themeName, themeProperties, options.projectStaticAssetsOutputFolder, logger);

const themeFile = generateThemeFile(themeFolder, themeName, themeProperties);

fs.writeFileSync(path.resolve(themeFolder, themeName + '.generated.js'), themeFile);
return true;
}
return false;
}

function getThemeProperties(themeFolder) {
const themePropertyFile = path.resolve(themeFolder, 'theme.json');
if (!fs.existsSync(themePropertyFile)) {
return {};
}
const themePropertyFileAsString = fs.readFileSync(themePropertyFile);
if (themePropertyFileAsString.length === 0) {
return {};
}
return JSON.parse(themePropertyFileAsString);
}

/**
* Extracts current theme name from 'theme-generated.js' file located on a
* given folder.
* @param themeFolder theme folder where flow generates 'theme-generated.js'
* file and copies local and jar resource frontend files
* @returns {string} current theme name
*/
function extractThemeName(themeFolder) {
if (!themeFolder) {
throw new Error("Couldn't extract theme name from 'theme-generated.js'," +
" because the path to folder containing this file is empty. Please set" +
" the a correct folder path in ApplicationThemePlugin constructor" +
" parameters.");
}
const generatedThemeFile = path.resolve(themeFolder, "theme-generated.js");
if (fs.existsSync(generatedThemeFile)) {
// read theme name from the theme-generated.js as there we always mark the used theme for webpack to handle.
const themeName = nameRegex.exec(fs.readFileSync(generatedThemeFile, {encoding: 'utf8'}))[1];
if (!themeName) {
throw new Error("Couldn't parse theme name from '" + generatedThemeFile + "'.");
}
return themeName;
} else {
return '';
}
}

module.exports = { processThemeResources, extractThemeName };
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"description": "theme-live-reload-plugin",
"keywords": [
"plugin",
"application theme",
"live-reload"
],
"repository": "vaadin/flow",
"name": "@vaadin/theme-live-reload-plugin",
"version": "1.0.0",
"main": "theme-live-reload-plugin.js",
"author": "Vaadin Ltd",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/vaadin/flow/issues"
},
"files": [
"theme-live-reload-plugin.js"
]
}

0 comments on commit b266396

Please sign in to comment.