diff --git a/flow-server/README.md b/flow-server/README.md
new file mode 100644
index 00000000000..2309d8a7deb
--- /dev/null
+++ b/flow-server/README.md
@@ -0,0 +1,79 @@
+## Flow Webpack plugins
+
+Flow now uses webpack plugins to make the `webpack.generated.js` cleaner and easier to extend
+without cluttering the file and making it long and complex.
+
+The files get installed with the task `TaskInstallWebpackPlugins` which reads the `webpack-plugins.json`
+in from `src/main/resources/plugins` and installs the plugins named here e.g.
+
+```json
+{
+ "plugins": [
+ "stats-plugin"
+ ]
+}
+```
+
+The plugin itself should also be contained in `src/main/resources/plugins` with the
+folder name being the same as the plugin name.
+
+For stats-plugin this means it should be located in `src/main/resources/plugins/stats-plugin`.
+
+The plugin folder needs to contain the plugin javascript files plus a package.json with at least the fields
+`version`, `main`, `files` filled where:
+ * `version` is the semver version for the plugin.
+ (Plugin will not be updated if the same version already exists)
+ * `main` depicts the main js file for the plugin.
+ * `files` contains all files the plugin needs.
+ (only these files will be copied)
+
+The full information would be preferred:
+
+```json
+{
+ "description": "stats-plugin",
+ "keywords": [
+ "plugin"
+ ],
+ "repository": "vaadin/flow",
+ "name": "@vaadin/stats-plugin",
+ "version": "1.0.0",
+ "main": "stats-plugin.js",
+ "author": "Vaadin Ltd",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/vaadin/flow/issues"
+ },
+ "files": [
+ "stats-plugin.js"
+ ]
+}
+```
+
+For creating a plugin see [Writing a plugin](https://webpack.js.org/contribute/writing-a-plugin/)
+
+## Using a Flow webpack plugin
+
+The flow plugins get installed to `node_modules/@vaadin` which means that using them we should use the for `@vaadin/${plugin-name}`
+
+As the plugins are meant for internal use the are added to `webpack.generated.js` and
+used from there.
+
+First we need to import the webpack plugin
+
+```js
+const StatsPlugin = require('@vaadin/stats-plugin');
+```
+
+then add the plugin with required options to `plugins` in the generated file
+```js
+ plugins: [
+ new StatsPlugin({
+ devMode: devMode,
+ statsFile: statsFile,
+ setResults: function (statsFile) {
+ stats = statsFile;
+ }
+ }),
+ ]
+```
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java
index c99f5c82539..ac9f7ffe89a 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java
@@ -30,10 +30,10 @@
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
import elemental.json.JsonObject;
-
import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_FRONTEND_DIR;
import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_GENERATED_DIR;
import static com.vaadin.flow.server.frontend.FrontendUtils.IMPORTS_NAME;
+import static com.vaadin.flow.server.frontend.FrontendUtils.NODE_MODULES;
import static com.vaadin.flow.server.frontend.FrontendUtils.PARAM_FRONTEND_DIR;
import static com.vaadin.flow.server.frontend.FrontendUtils.PARAM_GENERATED_DIR;
@@ -544,6 +544,9 @@ private NodeTasks(Builder builder) {
classFinder, packageUpdater,
builder.enablePnpm, builder.requireHomeNodeExec,
builder.nodeVersion, builder.nodeDownloadRoot));
+
+ commands.add(new TaskInstallWebpackPlugins(
+ new File(builder.npmFolder, NODE_MODULES)));
}
}
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskInstallWebpackPlugins.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskInstallWebpackPlugins.java
new file mode 100644
index 00000000000..e59256c3afe
--- /dev/null
+++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskInstallWebpackPlugins.java
@@ -0,0 +1,179 @@
+/*
+ * 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.
+ */
+package com.vaadin.flow.server.frontend;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import elemental.json.Json;
+import elemental.json.JsonArray;
+import elemental.json.JsonObject;
+import static com.vaadin.flow.server.Constants.PACKAGE_JSON;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Task that installs any Flow webpack plugins into node_modules/@vaadin for
+ * use with webpack compilation.
+ *
+ * This should preferably be executed after npm installation to not make it skip
+ * or have the plugins deleted by {@link TaskRunNpmInstall}.
+ *
+ * @since
+ */
+public class TaskInstallWebpackPlugins implements FallibleCommand {
+
+ private File nodeModulesFolder;
+
+ /**
+ * Copy Flow webpack plugins into the given nodeModulesFolder.
+ *
+ * @param nodeModulesFolder
+ * node_modules folder to copy files to
+ */
+ public TaskInstallWebpackPlugins(File nodeModulesFolder) {
+ this.nodeModulesFolder = nodeModulesFolder;
+ }
+
+ @Override
+ public void execute() {
+ getPlugins().forEach(plugin -> {
+ try {
+ generatePluginFiles(plugin);
+ } catch (IOException ioe) {
+ throw new UncheckedIOException(
+ "Installation of Flow webpack plugin '" + plugin
+ + "' failed", ioe);
+ }
+ });
+ }
+
+ /**
+ * Get names for plugins to install into node_modules.
+ *
+ * @return names of plugins to install
+ */
+ protected List getPlugins() {
+ try {
+ final JsonObject jsonFile = getJsonFile(
+ "plugins/webpack-plugins.json");
+ if (jsonFile == null) {
+ log().error(
+ "Couldn't locate plugins/webpack-plugins.json, no Webpack plugins for Flow will be installed."
+ + "If webpack build fails validate flow-server jar content.");
+ return Collections.emptyList();
+ }
+
+ final JsonArray plugins = jsonFile.getArray("plugins");
+ List pluginsToInstall = new ArrayList<>(plugins.length());
+ for (int i = 0; i < plugins.length(); i++) {
+ pluginsToInstall.add(plugins.getString(i));
+ }
+ return pluginsToInstall;
+ } catch (IOException ioe) {
+ throw new UncheckedIOException(
+ "Couldn't load webpack-plugins.json file", ioe);
+ }
+ }
+
+ private void generatePluginFiles(String pluginName) throws IOException {
+ // Get the target folder where the plugin should be installed to
+ File pluginTargetFile = new File(nodeModulesFolder,
+ "@vaadin/" + pluginName);
+
+ final String pluginFolderName = "plugins/" + pluginName + "/";
+ final JsonObject packageJson = getJsonFile(
+ pluginFolderName + PACKAGE_JSON);
+ if (packageJson == null) {
+ log().error(
+ "Couldn't locate '{}' for plugin '{}'. Plugin will not be installed.",
+ PACKAGE_JSON, pluginName);
+ return;
+ }
+
+ // Validate installed version and don't override if same
+ if (pluginTargetFile.exists() && new File(pluginTargetFile,
+ PACKAGE_JSON).exists()) {
+ String packageFile = FileUtils
+ .readFileToString(new File(pluginTargetFile, PACKAGE_JSON),
+ StandardCharsets.UTF_8);
+ final FrontendVersion packageVersion = new FrontendVersion(
+ Json.parse(packageFile).getString("version"));
+ FrontendVersion pluginVersion = new FrontendVersion(
+ packageJson.getString("version"));
+ if (packageVersion.isEqualTo(pluginVersion)) {
+ log().debug(
+ "Skipping install of {} for version {} already installed",
+ pluginName, pluginVersion.getFullVersion());
+ return;
+ }
+ }
+
+ // Create target folder if necessary
+ FileUtils.forceMkdir(pluginTargetFile);
+
+ // copy only files named in package.json { files }
+ final JsonArray files = packageJson.getArray("files");
+ for (int i = 0; i < files.length(); i++) {
+ final String file = files.getString(i);
+ FileUtils.copyURLToFile(getResourceUrl(pluginFolderName + file),
+ new File(pluginTargetFile, file));
+ }
+ // copy package.json to plugin directory
+ FileUtils.copyURLToFile(getResourceUrl(pluginFolderName + PACKAGE_JSON),
+ new File(pluginTargetFile, PACKAGE_JSON));
+ }
+
+ private JsonObject getJsonFile(String jsonFilePath) throws IOException {
+ final URL urlResource = getResourceUrl(jsonFilePath);
+ if (urlResource == null) {
+ return null;
+ }
+ File jsonFile = new File(urlResource.getFile());
+ String jsonString;
+ if (!jsonFile.exists()) {
+ try (InputStream resourceAsStream = this.getClass().getClassLoader()
+ .getResourceAsStream(jsonFilePath)) {
+ if (resourceAsStream != null) {
+ jsonString = FrontendUtils.streamToString(resourceAsStream);
+ } else {
+ return null;
+ }
+ }
+ } else {
+ jsonString = FileUtils.readFileToString(jsonFile, UTF_8);
+ }
+ return Json.parse(jsonString);
+ }
+
+ private URL getResourceUrl(String resource) {
+ return this.getClass().getClassLoader().getResource(resource);
+ }
+
+ private Logger log() {
+ return LoggerFactory.getLogger(this.getClass());
+ }
+}
diff --git a/flow-server/src/main/resources/plugins/stats-plugin/package.json b/flow-server/src/main/resources/plugins/stats-plugin/package.json
new file mode 100644
index 00000000000..314db4653e2
--- /dev/null
+++ b/flow-server/src/main/resources/plugins/stats-plugin/package.json
@@ -0,0 +1,18 @@
+{
+ "description": "stats-plugin",
+ "keywords": [
+ "plugin"
+ ],
+ "repository": "vaadin/flow",
+ "name": "@vaadin/stats-plugin",
+ "version": "1.0.0",
+ "main": "stats-plugin.js",
+ "author": "Vaadin Ltd",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/vaadin/flow/issues"
+ },
+ "files": [
+ "stats-plugin.js"
+ ]
+}
diff --git a/flow-server/src/main/resources/plugins/stats-plugin/stats-plugin.js b/flow-server/src/main/resources/plugins/stats-plugin/stats-plugin.js
new file mode 100644
index 00000000000..75f317cf54a
--- /dev/null
+++ b/flow-server/src/main/resources/plugins/stats-plugin/stats-plugin.js
@@ -0,0 +1,162 @@
+/*
+ * 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.
+ */
+const fs = require('fs');
+const mkdirp = require('mkdirp');
+const path = require('path');
+
+/**
+ * This plugin handles minimization of the stats.json file to only contain required info to keep
+ * the file as small as possible.
+ *
+ * In production mode the stats.json is written to the file system, in dev mode the json is returned to
+ * the webpack callback function.
+ */
+class StatsPlugin {
+
+ constructor(options = {}) {
+ this.options = options;
+ }
+
+ apply(compiler) {
+ const logger = compiler.getInfrastructureLogger("FlowIdPlugin");
+
+ compiler.hooks.afterEmit.tapAsync("FlowIdPlugin", (compilation, done) => {
+ let statsJson = compilation.getStats().toJson();
+ // Get bundles as accepted keys
+ let acceptedKeys = statsJson.assets.filter(asset => asset.chunks.length > 0)
+ .map(asset => asset.chunks).reduce((acc, val) => acc.concat(val), []);
+
+ // Collect all modules for the given keys
+ const modules = collectModules(statsJson, acceptedKeys);
+
+ // Collect accepted chunks and their modules
+ const chunks = collectChunks(statsJson, acceptedKeys);
+
+ let customStats = {
+ hash: statsJson.hash,
+ assetsByChunkName: statsJson.assetsByChunkName,
+ chunks: chunks,
+ modules: modules
+ };
+
+ if (!this.options.devMode) {
+ // eslint-disable-next-line no-console
+ logger.info("Emitted " + this.options.statsFile);
+ mkdirp(path.dirname(this.options.statsFile));
+ fs.writeFile(this.options.statsFile, JSON.stringify(customStats, null, 1), done);
+ } else {
+ // eslint-disable-next-line no-console
+ logger.info("Serving the 'stats.json' file dynamically.");
+
+ this.options.setResults(customStats);
+ done();
+ }
+ });
+ }
+}
+
+module.exports = StatsPlugin;
+
+/**
+ * Collect chunk data for accepted chunk ids.
+ * @param statsJson full stats.json content
+ * @param acceptedKeys chunk ids that are accepted
+ * @returns slimmed down chunks
+ */
+function collectChunks(statsJson, acceptedChunks) {
+ const chunks = [];
+ // only handle chunks if they exist for stats
+ if (statsJson.chunks) {
+ statsJson.chunks.forEach(function (chunk) {
+ // Acc chunk if chunk id is in accepted chunks
+ if (acceptedChunks.includes(chunk.id)) {
+ const modules = [];
+ // Add all modules for chunk as slimmed down modules
+ chunk.modules.forEach(function (module) {
+ const slimModule = {
+ id: module.id,
+ name: module.name,
+ source: module.source
+ };
+ if (module.modules) {
+ slimModule.modules = collectSubModules(module);
+ }
+ modules.push(slimModule);
+ });
+ const slimChunk = {
+ id: chunk.id,
+ names: chunk.names,
+ files: chunk.files,
+ hash: chunk.hash,
+ modules: modules
+ }
+ chunks.push(slimChunk);
+ }
+ });
+ }
+ return chunks;
+}
+
+/**
+ * Collect all modules that are for a chunk in acceptedChunks.
+ * @param statsJson full stats.json
+ * @param acceptedChunks chunk names that are accepted for modules
+ * @returns slimmed down modules
+ */
+function collectModules(statsJson, acceptedChunks) {
+ let modules = [];
+ // skip if no modules defined
+ if (statsJson.modules) {
+ statsJson.modules.forEach(function (module) {
+ // Add module if module chunks contain an accepted chunk and the module is generated-flow-imports.js module
+ if (module.chunks.filter(key => acceptedChunks.includes(key)).length > 0 &&
+ (module.name.includes("generated-flow-imports.js") || module.name.includes("generated-flow-imports-fallback.js"))) {
+ const slimModule = {
+ id: module.id,
+ name: module.name,
+ source: module.source
+ };
+ if (module.modules) {
+ slimModule.modules = collectSubModules(module);
+ }
+ modules.push(slimModule);
+ }
+ });
+ }
+ return modules;
+}
+
+/**
+ * Collect any modules under a module (aka. submodules);
+ *
+ * @param module module to get submodules for
+ */
+function collectSubModules(module) {
+ let modules = [];
+ module.modules.forEach(function (submodule) {
+ if (submodule.source) {
+ const slimModule = {
+ name: submodule.name,
+ source: submodule.source,
+ };
+ if (submodule.id) {
+ slimModule.id = submodule.id;
+ }
+ modules.push(slimModule);
+ }
+ });
+ return modules;
+}
diff --git a/flow-server/src/main/resources/plugins/webpack-plugins.json b/flow-server/src/main/resources/plugins/webpack-plugins.json
new file mode 100644
index 00000000000..ee2c9be8926
--- /dev/null
+++ b/flow-server/src/main/resources/plugins/webpack-plugins.json
@@ -0,0 +1,5 @@
+{
+ "plugins": [
+ "stats-plugin"
+ ]
+}
diff --git a/flow-server/src/main/resources/webpack.generated.js b/flow-server/src/main/resources/webpack.generated.js
index f6560b5d746..eed1a658d52 100644
--- a/flow-server/src/main/resources/webpack.generated.js
+++ b/flow-server/src/main/resources/webpack.generated.js
@@ -9,6 +9,9 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
+// Flow plugins
+const StatsPlugin = require('@vaadin/stats-plugin');
+
const path = require('path');
// the folder of app resources:
@@ -150,40 +153,13 @@ module.exports = {
// Generate compressed bundles when not devMode
!devMode && new CompressionPlugin(),
- // Generates the stats file for flow `@Id` binding.
- function (compiler) {
- compiler.hooks.afterEmit.tapAsync("FlowIdPlugin", (compilation, done) => {
- let statsJson = compilation.getStats().toJson();
- // Get bundles as accepted keys
- let acceptedKeys = statsJson.assets.filter(asset => asset.chunks.length > 0)
- .map(asset => asset.chunks).reduce((acc, val) => acc.concat(val), []);
-
- // Collect all modules for the given keys
- const modules = collectModules(statsJson, acceptedKeys);
-
- // Collect accepted chunks and their modules
- const chunks = collectChunks(statsJson, acceptedKeys);
-
- let customStats = {
- hash: statsJson.hash,
- assetsByChunkName: statsJson.assetsByChunkName,
- chunks: chunks,
- modules: modules
- };
-
- if (!devMode) {
- // eslint-disable-next-line no-console
- console.log(" Emitted " + statsFile);
- fs.writeFile(statsFile, JSON.stringify(customStats, null, 1), done);
- } else {
- // eslint-disable-next-line no-console
- console.log(" Serving the 'stats.json' file dynamically.");
-
- stats = customStats;
- done();
- }
- });
- },
+ new StatsPlugin({
+ devMode: devMode,
+ statsFile: statsFile,
+ setResults: function (statsFile) {
+ stats = statsFile;
+ }
+ }),
// Includes JS output bundles into "index.html"
useClientSideIndexFileForBootstrapping && new HtmlWebpackPlugin({
@@ -196,94 +172,3 @@ module.exports = {
}),
].filter(Boolean)
};
-
-/**
- * Collect chunk data for accepted chunk ids.
- * @param statsJson full stats.json content
- * @param acceptedKeys chunk ids that are accepted
- * @returns slimmed down chunks
- */
-function collectChunks(statsJson, acceptedChunks) {
- const chunks = [];
- // only handle chunks if they exist for stats
- if (statsJson.chunks) {
- statsJson.chunks.forEach(function (chunk) {
- // Acc chunk if chunk id is in accepted chunks
- if (acceptedChunks.includes(chunk.id)) {
- const modules = [];
- // Add all modules for chunk as slimmed down modules
- chunk.modules.forEach(function (module) {
- const slimModule = {
- id: module.id,
- name: module.name,
- source: module.source
- };
- if(module.modules) {
- slimModule.modules = collectSubModules(module);
- }
- modules.push(slimModule);
- });
- const slimChunk = {
- id: chunk.id,
- names: chunk.names,
- files: chunk.files,
- hash: chunk.hash,
- modules: modules
- }
- chunks.push(slimChunk);
- }
- });
- }
- return chunks;
-}
-
-/**
- * Collect all modules that are for a chunk in acceptedChunks.
- * @param statsJson full stats.json
- * @param acceptedChunks chunk names that are accepted for modules
- * @returns slimmed down modules
- */
-function collectModules(statsJson, acceptedChunks) {
- let modules = [];
- // skip if no modules defined
- if (statsJson.modules) {
- statsJson.modules.forEach(function (module) {
- // Add module if module chunks contain an accepted chunk and the module is generated-flow-imports.js module
- if (module.chunks.filter(key => acceptedChunks.includes(key)).length > 0
- && (module.name.includes("generated-flow-imports.js") || module.name.includes("generated-flow-imports-fallback.js"))) {
- const slimModule = {
- id: module.id,
- name: module.name,
- source: module.source
- };
- if(module.modules) {
- slimModule.modules = collectSubModules(module);
- }
- modules.push(slimModule);
- }
- });
- }
- return modules;
-}
-
-/**
- * Collect any modules under a module (aka. submodules);
- *
- * @param module module to get submodules for
- */
-function collectSubModules(module) {
- let modules = [];
- module.modules.forEach(function (submodule) {
- if (submodule.source) {
- const slimModule = {
- name: submodule.name,
- source: submodule.source,
- };
- if(submodule.id) {
- slimModule.id = submodule.id;
- }
- modules.push(slimModule);
- }
- });
- return modules;
-}
diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskInstallWebpackPluginsTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskInstallWebpackPluginsTest.java
new file mode 100644
index 00000000000..522f3cc2d99
--- /dev/null
+++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskInstallWebpackPluginsTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+
+package com.vaadin.flow.server.frontend;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.FilenameUtils;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import elemental.json.Json;
+import elemental.json.JsonArray;
+import elemental.json.JsonObject;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+public class TaskInstallWebpackPluginsTest {
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private File nodeModulesFolder;
+
+ private TaskInstallWebpackPlugins task;
+
+ @Before
+ public void init() throws IOException {
+ nodeModulesFolder = temporaryFolder.newFolder();
+ task = new TaskInstallWebpackPlugins(nodeModulesFolder);
+ }
+
+ @Test
+ public void getPluginsReturnsExpectedList() {
+ String[] expectedPlugins = new String[] { "stats-plugin" };
+ final List plugins = task.getPlugins();
+ Assert.assertEquals(
+ "Unexpected amount of plugins in 'webpack-plugins.json'",
+ expectedPlugins.length, plugins.size());
+
+ for (String plugin : expectedPlugins) {
+ Assert.assertTrue(
+ "'webpack-plugins.json' didn't contain '" + plugin + "'",
+ plugins.contains(plugin));
+ }
+ }
+
+ @Test
+ public void webpackPluginsAreCopied() throws IOException {
+ task.execute();
+
+ assertPlugins();
+ }
+
+ @Test
+ public void pluginsDefineAllScriptFiles() throws IOException {
+ for (String plugin : task.getPlugins()) {
+ verifyPluginScriptFilesAreDefined(plugin);
+ }
+ }
+
+ private void assertPlugins() throws IOException {
+ Assert.assertTrue("No @vaadin folder created",
+ new File(nodeModulesFolder, "@vaadin").exists());
+ for (String plugin : task.getPlugins()) {
+ assertPlugin(plugin);
+ }
+ }
+
+ private void assertPlugin(String plugin) throws IOException {
+ final File pluginFolder = getPluginFolder(plugin);
+
+ final JsonArray files = getPluginFiles(pluginFolder);
+ for (int i = 0; i < files.length(); i++) {
+ Assert.assertTrue(
+ "Missing plugin file " + files.getString(i) + " for " + plugin,
+ new File(pluginFolder, files.getString(i)).exists());
+ }
+ }
+
+ private void verifyPluginScriptFilesAreDefined(String plugin)
+ throws IOException {
+ final File pluginFolder = new File(
+ this.getClass().getClassLoader().getResource("plugins/" + plugin)
+ .getFile());
+
+ final JsonArray files = getPluginFiles(pluginFolder);
+ List fileNames = new ArrayList<>(files.length());
+ for (int i = 0; i < files.length(); i++) {
+ Assert.assertTrue(
+ "Missing plugin file " + files.getString(i) + " for " + plugin,
+ new File(pluginFolder, files.getString(i)).exists());
+ fileNames.add(files.getString(i));
+ }
+ final List pluginFiles = Arrays.stream(pluginFolder.listFiles(
+ (dir, name) -> FilenameUtils.getExtension(name).equals("js")))
+ .map(file -> file.getName()).collect(Collectors.toList());
+ for (String fileName : pluginFiles) {
+ Assert.assertTrue(String.format(
+ "Plugin '%s' doesn't define script file '%s' in package.json files",
+ plugin, fileName), fileNames.contains(fileName));
+ }
+ }
+
+ /**
+ * Get the expected plugin files from package.json
+ *
+ * @param pluginFolder
+ * @return
+ * @throws IOException
+ */
+ private JsonArray getPluginFiles(File pluginFolder) throws IOException {
+ final JsonObject packageJson = Json.parse(FileUtils
+ .readFileToString(new File(pluginFolder, "package.json"), UTF_8));
+ return packageJson.getArray("files");
+ }
+
+ private File getPluginFolder(String plugin) {
+ final String pluginString = "@vaadin" + File.separator + plugin;
+ final File pluginFolder = new File(nodeModulesFolder, pluginString);
+
+ Assert.assertTrue("Missing plugin folder for " + plugin,
+ pluginFolder.exists());
+ Assert.assertTrue("Missing package.json for " + plugin,
+ new File(pluginFolder, "package.json").exists());
+ return pluginFolder;
+ }
+
+}
diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java
index 4828089c60f..32d66cc11f2 100644
--- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskRunNpmInstallTest.java
@@ -69,7 +69,6 @@ public class TaskRunNpmInstallTest {
@Before
public void setUp() throws IOException {
- File generatedFolder = temporaryFolder.newFolder();
npmFolder = temporaryFolder.newFolder();
nodeUpdater = new NodeUpdater(Mockito.mock(ClassFinder.class),
Mockito.mock(FrontendDependencies.class), npmFolder,
@@ -217,7 +216,7 @@ public void writeLocalHash(String hash) throws IOException {
}
@Test
- public void runNpmInstall_dirContainsOnlyFlowNpmPackage_npmInstallIsNotExecuted()
+ public void runNpmInstall_dirContainsOnlyFlowNpmPackage_npmInstallIsExecuted()
throws ExecutionFailedException {
File nodeModules = getNodeUpdater().nodeModulesFolder;
nodeModules.mkdir();
diff --git a/flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java b/flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java
index cdc15bb1a59..91cee36db51 100644
--- a/flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java
+++ b/flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java
@@ -190,6 +190,7 @@ protected Stream getExcludedPatterns() {
"com\\.vaadin\\.flow\\.server\\.frontend\\.TaskGenerateIndexHtml",
"com\\.vaadin\\.flow\\.server\\.frontend\\.TaskGenerateIndexTs",
"com\\.vaadin\\.flow\\.server\\.frontend\\.TaskGenerateTsDefinitions",
+ "com\\.vaadin\\.flow\\.server\\.frontend\\.TaskInstallWebpackPlugins",
// Node downloader classes
"com\\.vaadin\\.flow\\.server\\.frontend\\.installer\\.DefaultArchiveExtractor",