Skip to content

Commit

Permalink
feat: Create Flow plugins for webpack (#9295)
Browse files Browse the repository at this point in the history
Moved stats file handling to a custom plugin.
Added feature for copying custom Flow plugins
for use with webpack.

Fixes #9283
  • Loading branch information
caalador committed Feb 12, 2021
1 parent 9d24be5 commit f442e76
Show file tree
Hide file tree
Showing 9 changed files with 615 additions and 115 deletions.
79 changes: 79 additions & 0 deletions flow-server/README.md
Original file line number Diff line number Diff line change
@@ -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;
}
}),
]
```
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,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;

Expand Down Expand Up @@ -465,6 +465,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)));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<String> 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<String> 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());
}
}
18 changes: 18 additions & 0 deletions flow-server/src/main/resources/plugins/stats-plugin/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}

0 comments on commit f442e76

Please sign in to comment.