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",