3236/vite migration (#3354)

* Migrate to Vite
* Remove asset-manifest.json which vite doesn't generate
* Fix tree-shaking removing Export URL if-blocks from services
* Implement address correction for the vite bundle
* Fixed issue with single proxy online graph 403 without page.server.overview.players.online.graph

Affects issues:
- Close #3236
This commit is contained in:
Aurora Lahtela 2023-12-10 09:29:01 +02:00 committed by GitHub
parent 05cf96de0e
commit bc424f062f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
214 changed files with 615 additions and 8122 deletions

View File

@ -113,13 +113,14 @@ task updateVersion(type: Copy) {
node {
download = true
version = "16.14.2"
version = "20.9.0"
nodeProjectDir = file("$rootDir/react/dashboard")
}
task yarnBundle(type: YarnTask) {
inputs.files(fileTree("$rootDir/react/dashboard/src"))
inputs.file("$rootDir/react/dashboard/package.json")
inputs.file("$rootDir/react/dashboard/vite.config.js")
outputs.dir("$rootDir/react/dashboard/build")

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.delivery.export;
import com.djrapitops.plan.delivery.rendering.BundleAddressCorrection;
import com.djrapitops.plan.delivery.web.AssetVersions;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
@ -52,23 +53,25 @@ public class ReactExporter extends FileExporter {
private final PlanConfig config;
private final RootJSONResolver jsonHandler;
private final AssetVersions assetVersions;
private final BundleAddressCorrection bundleAddressCorrection;
@Inject
public ReactExporter(
PlanFiles files,
PlanConfig config,
RootJSONResolver jsonHandler,
AssetVersions assetVersions
AssetVersions assetVersions,
BundleAddressCorrection bundleAddressCorrection
) {
this.files = files;
this.config = config;
this.jsonHandler = jsonHandler;
this.assetVersions = assetVersions;
this.bundleAddressCorrection = bundleAddressCorrection;
}
public void exportReactFiles(Path toDirectory) throws IOException {
exportIndexHtml(toDirectory);
exportAsset(toDirectory, "asset-manifest.json");
exportAsset(toDirectory, "favicon.ico");
exportAsset(toDirectory, "logo192.png");
exportAsset(toDirectory, "logo512.png");
@ -104,17 +107,18 @@ public class ReactExporter extends FileExporter {
Path to = toDirectory.resolve(path);
Resource resource = files.getResourceFromJar("web/" + path);
// Make static asset loading work with subdirectory addresses
if (path.endsWith(".css") || "asset-manifest.json".equals(path)) {
if (path.endsWith(".css")) {
String contents = resource.asString();
String withReplacedStatic = StringUtils.replace(contents, "/static", getBasePath() + "/static");
export(to, withReplacedStatic);
String withReplaced = bundleAddressCorrection.correctAddressForExport(contents, path);
export(to, withReplaced);
} else if (path.endsWith(".js")) {
String withReplacedConstants = StringUtils.replaceEach(
resource.asString(),
new String[]{"PLAN_BASE_ADDRESS", "PLAN_EXPORTED_VERSION", ".p=\"/\""},
new String[]{config.get(WebserverSettings.EXTERNAL_LINK), "true", ".p=\"" + getBasePath() + "/\""}
new String[]{"PLAN_BASE_ADDRESS", "PLAN_EXPORTED_VERSION"},
new String[]{config.get(WebserverSettings.EXTERNAL_LINK), "true"}
);
export(to, withReplacedConstants);
String withReplaced = bundleAddressCorrection.correctAddressForExport(withReplacedConstants, path);
export(to, withReplaced);
} else {
export(to, resource);
}
@ -141,25 +145,11 @@ public class ReactExporter extends FileExporter {
private void exportIndexHtml(Path toDirectory) throws IOException {
String contents = files.getResourceFromJar("web/index.html")
.asString();
String basePath = getBasePath();
contents = StringUtils.replaceEach(contents,
new String[]{"/static", "/pageExtensionApi.js"},
new String[]{basePath + "/static", basePath + "/pageExtensionApi.js"});
contents = bundleAddressCorrection.correctAddressForExport(contents, "index.html");
export(toDirectory.resolve("index.html"), contents);
}
private String getBasePath() {
String basePath = config.get(WebserverSettings.EXTERNAL_LINK)
.replace("http://", "")
.replace("https://", "");
if (StringUtils.contains(basePath, '/')) {
return basePath.substring(StringUtils.indexOf(basePath, '/'));
} else {
return "";
}
}
private void exportAsset(Path toDirectory, String asset) throws IOException {
export(toDirectory.resolve(asset), files.getResourceFromJar("web/" + asset));
}

View File

@ -0,0 +1,155 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* In charge of correcting the root address in the javascript bundle.
* <p>
* The javascript bundle assumes everything is hosted at /,
* but hosting settings affect the address and it could be hosted at a subdirectory like /plan/
*
* @author AuroraLS3
*/
@Singleton
public class BundleAddressCorrection {
private static final String STATIC = "static";
private static final Pattern JAVASCRIPT_ADDRESS_PATTERN = Pattern.compile("\"(\\./|/?static)(.+?)\\.(json|js|css)\"");
private final PlanConfig config;
private final Addresses addresses;
@Inject
public BundleAddressCorrection(PlanConfig config, Addresses addresses) {
this.config = config;
this.addresses = addresses;
}
private String getExportBasePath() {
return addresses.getBasePath(config.get(WebserverSettings.EXTERNAL_LINK));
}
private String getWebserverBasePath() {
String address = addresses.getMainAddress()
.orElseGet(addresses::getFallbackLocalhostAddress);
return addresses.getBasePath(address);
}
public String correctAddressForWebserver(String content, String fileName) {
String basePath = getWebserverBasePath();
return correctAddress(content, fileName, basePath);
}
public String correctAddressForExport(String content, String fileName) {
String basePath = getExportBasePath();
return correctAddress(content, fileName, basePath);
}
// basePath is either empty if the address doesn't have a subdirectory, or a subdirectory.
@Nullable
private String correctAddress(String content, String fileName, String basePath) {
if (fileName.endsWith(".css")) {
return correctAddressInCss(content, basePath);
} else if (fileName.endsWith(".js")) {
return correctAddressInJavascript(content, basePath);
} else if ("index.html".equals(fileName)) {
return correctAddressInHtml(content, basePath);
}
return content;
}
private String correctAddressInHtml(String content, String basePath) {
String endingSlash = basePath.endsWith("/") ? "" : "/";
return StringUtils.replaceEach(content,
new String[]{"src=\"/", "href=\"/"},
new String[]{"src=\"" + basePath + endingSlash, "href=\"" + basePath + endingSlash});
}
private String correctAddressInCss(String content, String basePath) {
String endingSlash = basePath.endsWith("/") ? "" : "/";
return StringUtils.replace(content, "/static", basePath + endingSlash + STATIC);
}
private String correctAddressInJavascript(String content, String basePath) {
int lastIndex = 0;
StringBuilder output = new StringBuilder();
Matcher matcher = JAVASCRIPT_ADDRESS_PATTERN.matcher(content);
while (matcher.find()) {
String addressStart = matcher.group(1);
String file = matcher.group(2);
String extension = matcher.group(3);
int startIndex = matcher.start();
int endIndex = matcher.end();
// If basePath is empty the website is hosted at root of the tree /
boolean atUrlRoot = basePath.isEmpty();
// This handles /static and static representation
boolean startsWithSlash = addressStart.startsWith("/");
String startingSlash = startsWithSlash ? "/" : "";
// This handles basePath containing a slash after subdirectory, such as /plan/ instead of /plan
String endingSlash = basePath.endsWith("/") ? "" : "/";
// Without subdirectory we can use the address as is, and it doesn't need changes,
// otherwise we can add the directory to the start.
String staticReplacement = atUrlRoot
? startingSlash + STATIC
: basePath + endingSlash + STATIC;
String relativeReplacement = atUrlRoot
? "./"
: basePath + endingSlash + "static/";
// Replaces basePath starting slash if the replaced thing does not start with slash
if (!startsWithSlash && staticReplacement.startsWith("/")) {
staticReplacement = staticReplacement.substring(1);
}
// Replacement examples when basepath is empty, "/plan" or "/plan/"
// "./Filename-hash.js" -> "./Filename-hash.js" or "/plan/static/Filename-hash.js"
// "/static/Filename-hash.js" -> "/static/Filename-hash.js" or "/plan/static/Filename-hash.js"
// "static/Filename-hash.js" -> "static/Filename-hash.js" or "plan/static/Filename-hash.js"
String replacementAddress = StringUtils.equalsAny(addressStart, "/static", STATIC)
? staticReplacement
: relativeReplacement;
String replacement = '"' + replacementAddress + file + '.' + extension + '"';
output.append(content, lastIndex, startIndex) // Append non-match
.append(replacement); // Append replaced address
lastIndex = endIndex;
}
// Append rest of the content that didn't match
if (lastIndex < content.length()) {
output.append(content, lastIndex, content.length());
}
return output.toString();
}
}

View File

@ -16,11 +16,11 @@
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.delivery.rendering.BundleAddressCorrection;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
import com.djrapitops.plan.delivery.web.resource.WebResource;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.theme.Theme;
@ -50,7 +50,7 @@ public class PageFactory {
private final Lazy<PublicHtmlFiles> publicHtmlFiles;
private final Lazy<Theme> theme;
private final Lazy<DBSystem> dbSystem;
private final Lazy<Addresses> addresses;
private final Lazy<BundleAddressCorrection> bundleAddressCorrection;
private static final String ERROR_HTML_FILE = "error.html";
@Inject
@ -61,14 +61,14 @@ public class PageFactory {
Lazy<Theme> theme,
Lazy<DBSystem> dbSystem,
Lazy<ServerInfo> serverInfo,
Lazy<Addresses> addresses
Lazy<BundleAddressCorrection> bundleAddressCorrection
) {
this.versionChecker = versionChecker;
this.files = files;
this.publicHtmlFiles = publicHtmlFiles;
this.theme = theme;
this.dbSystem = dbSystem;
this.addresses = addresses;
this.bundleAddressCorrection = bundleAddressCorrection;
}
public Page playersPage() throws IOException {
@ -81,18 +81,12 @@ public class PageFactory {
WebResource resource = ResourceService.getInstance().getResource(
"Plan", fileName, () -> getPublicHtmlOrJarResource(fileName)
);
return new ReactPage(getBasePath(), resource);
return new ReactPage(bundleAddressCorrection.get(), resource);
} catch (UncheckedIOException readFail) {
throw readFail.getCause();
}
}
private String getBasePath() {
String address = addresses.get().getMainAddress()
.orElseGet(addresses.get()::getFallbackLocalhostAddress);
return addresses.get().getBasePath(address);
}
/**
* Create a server page.
*

View File

@ -16,8 +16,8 @@
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.delivery.rendering.BundleAddressCorrection;
import com.djrapitops.plan.delivery.web.resource.WebResource;
import org.apache.commons.lang3.StringUtils;
/**
* Represents React index.html.
@ -26,20 +26,17 @@ import org.apache.commons.lang3.StringUtils;
*/
public class ReactPage implements Page {
private final String basePath;
private final BundleAddressCorrection bundleAddressCorrection;
private final WebResource reactHtml;
public ReactPage(String basePath, WebResource reactHtml) {
this.basePath = basePath;
public ReactPage(BundleAddressCorrection bundleAddressCorrection, WebResource reactHtml) {
this.bundleAddressCorrection = bundleAddressCorrection;
this.reactHtml = reactHtml;
}
@Override
public String toHtml() {
return StringUtils.replaceEach(
reactHtml.asString(),
new String[]{"/static", "/pageExtensionApi.js"},
new String[]{basePath + "/static", basePath + "/pageExtensionApi.js"});
return bundleAddressCorrection.correctAddressForWebserver(reactHtml.asString(), "index.html");
}
@Override

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.delivery.domain.container.PlayerContainer;
import com.djrapitops.plan.delivery.domain.keys.PlayerKeys;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.BundleAddressCorrection;
import com.djrapitops.plan.delivery.rendering.html.icon.Family;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.delivery.rendering.pages.Page;
@ -75,6 +76,7 @@ public class ResponseFactory {
private final DBSystem dbSystem;
private final Theme theme;
private final Lazy<Addresses> addresses;
private final Lazy<BundleAddressCorrection> bundleAddressCorrection;
private final Formatter<Long> httpLastModifiedFormatter;
@Inject
@ -86,7 +88,8 @@ public class ResponseFactory {
DBSystem dbSystem,
Formatters formatters,
Theme theme,
Lazy<Addresses> addresses
Lazy<Addresses> addresses,
Lazy<BundleAddressCorrection> bundleAddressCorrection
) {
this.files = files;
this.publicHtmlFiles = publicHtmlFiles;
@ -97,6 +100,7 @@ public class ResponseFactory {
this.addresses = addresses;
httpLastModifiedFormatter = formatters.httpLastModifiedLong();
this.bundleAddressCorrection = bundleAddressCorrection;
}
/**
@ -232,9 +236,7 @@ public class ResponseFactory {
String content = UnaryChain.of(resource.asString())
.chain(this::replaceMainAddressPlaceholder)
.chain(theme::replaceThemeColors)
.chain(contents -> StringUtils.replace(contents,
".p=\"/\"",
".p=\"" + getBasePath() + "/\""))
.chain(contents -> bundleAddressCorrection.get().correctAddressForWebserver(contents, fileName))
.apply();
ResponseBuilder responseBuilder = Response.builder()
.setMimeType(MimeType.JS)
@ -244,7 +246,7 @@ public class ResponseFactory {
if (fileName.contains(STATIC_BUNDLE_FOLDER)) {
resource.getLastModified().ifPresent(lastModified -> responseBuilder
// Can't cache main bundle in browser since base path might change
.setHeader(HttpHeader.CACHE_CONTROL.asString(), fileName.contains("main") ? CacheStrategy.CHECK_ETAG : CacheStrategy.CACHE_IN_BROWSER)
.setHeader(HttpHeader.CACHE_CONTROL.asString(), fileName.contains("index") ? CacheStrategy.CHECK_ETAG : CacheStrategy.CACHE_IN_BROWSER)
.setHeader(HttpHeader.LAST_MODIFIED.asString(), httpLastModifiedFormatter.apply(lastModified))
.setHeader(HttpHeader.ETAG.asString(), lastModified));
}
@ -254,12 +256,6 @@ public class ResponseFactory {
}
}
private String getBasePath() {
String address = addresses.get().getMainAddress()
.orElseGet(addresses.get()::getFallbackLocalhostAddress);
return addresses.get().getBasePath(address);
}
private String replaceMainAddressPlaceholder(String resource) {
String address = addresses.get().getAccessAddress()
.orElseGet(addresses.get()::getFallbackLocalhostAddress);
@ -275,7 +271,7 @@ public class ResponseFactory {
WebResource resource = getPublicOrJarResource(fileName);
String content = UnaryChain.of(resource.asString())
.chain(theme::replaceThemeColors)
.chain(contents -> StringUtils.replace(contents, "/static", getBasePath() + "/static"))
.chain(contents -> bundleAddressCorrection.get().correctAddressForWebserver(contents, fileName))
.apply();
ResponseBuilder responseBuilder = Response.builder()

View File

@ -147,8 +147,9 @@ public class ResponseResolver {
String plugin = "Plan";
resolverService.registerResolver(plugin, "/robots.txt", fileResolver(responseFactory::robotsResponse));
resolverService.registerResolver(plugin, "/manifest.json", fileResolver(() -> responseFactory.jsonFileResponse("manifest.json")));
resolverService.registerResolver(plugin, "/asset-manifest.json", fileResolver(() -> responseFactory.jsonFileResponse("asset-manifest.json")));
resolverService.registerResolver(plugin, "/favicon.ico", fileResolver(responseFactory::faviconResponse));
resolverService.registerResolver(plugin, "/logo192.png", fileResolver(() -> responseFactory.imageResponse("logo192.png")));
resolverService.registerResolver(plugin, "/logo512.png", fileResolver(() -> responseFactory.imageResponse("logo512.png")));
resolverService.registerResolver(plugin, "/pageExtensionApi.js", fileResolver(() -> responseFactory.javaScriptResponse("pageExtensionApi.js")));
resolverService.registerResolver(plugin, "/query", queryPageResolver);

View File

@ -37,7 +37,7 @@ import java.util.Optional;
@Singleton
public class StaticResourceResolver implements NoAuthResolver {
private static final String PART_REGEX = "(vendor|css|js|img|static)";
private static final String PART_REGEX = "(static)";
public static final String PATH_REGEX = "^.*/" + PART_REGEX + "/.*";
private final ResponseFactory responseFactory;

View File

@ -214,7 +214,7 @@ public class GraphsJSONResolver extends JSONResolver {
case GRAPH_OPTIMIZED_PERFORMANCE:
return List.of(WebPermission.PAGE_SERVER_PERFORMANCE_GRAPHS, WebPermission.PAGE_NETWORK_PERFORMANCE);
case GRAPH_ONLINE:
return List.of(WebPermission.PAGE_SERVER_OVERVIEW_PLAYERS_ONLINE_GRAPH);
return List.of(WebPermission.PAGE_SERVER_OVERVIEW_PLAYERS_ONLINE_GRAPH, WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_ONLINE);
case GRAPH_UNIQUE_NEW:
return List.of(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_DAY_BY_DAY);
case GRAPH_HOURLY_UNIQUE_NEW:

View File

@ -106,7 +106,7 @@ public class ExportTestUtilities {
assertFalse(driver.findElement(By.tagName("body")).getText().contains("Bad Gateway"), "502 Bad Gateway, nginx could not reach Plan");
Awaitility.await()
Awaitility.await("waitForElementToBeVisible .load-in")
.atMost(Duration.of(10, ChronoUnit.SECONDS))
.until(() -> getMainPageElement(driver).map(WebElement::isDisplayed).orElse(false));

View File

@ -28,6 +28,7 @@ import com.djrapitops.plan.settings.config.paths.DataGatheringSettings;
import com.djrapitops.plan.settings.config.paths.DisplaySettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
import com.djrapitops.plan.storage.database.transactions.StoreServerInformationTransaction;
import com.djrapitops.plan.storage.database.transactions.commands.StoreWebUserTransaction;
import com.djrapitops.plan.storage.database.transactions.events.StoreServerPlayerTransaction;
@ -47,6 +48,7 @@ import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.logging.LogEntry;
import org.openqa.selenium.logging.LogType;
import org.testcontainers.shaded.org.awaitility.Awaitility;
import utilities.RandomData;
import utilities.TestConstants;
import utilities.TestResources;
@ -57,6 +59,7 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -291,8 +294,11 @@ class AccessControlVisibilityTest {
private void registerProxy(Database database) throws ExecutionException, InterruptedException {
database.executeTransaction(new StoreServerInformationTransaction(
new Server(TestConstants.SERVER_TWO_UUID, "Proxy", "https://localhost", TestConstants.VERSION)
new Server(null, TestConstants.SERVER_TWO_UUID, "Proxy", "https://localhost", true, TestConstants.VERSION)
)).get();
Awaitility.await("Proxy was not registered")
.atMost(5, TimeUnit.SECONDS)
.until(() -> !database.query(ServerQueries.fetchProxyServers()).isEmpty());
}
@DisplayName("Network element is visible with permission")

View File

@ -11,27 +11,18 @@
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link href="%PUBLIC_URL%/manifest.json" rel="manifest"/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
<link href="/manifest.json" rel="manifest"/>
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Plan | Player Analytics</title>
<link crossorigin="anonymous"
href="https://fonts.googleapis.com/css?family=Nunito:400,700,800,900&display=swap&subset=latin-ext"
rel="stylesheet">
</head>
<body>
<script src="%PUBLIC_URL%/pageExtensionApi.js"></script>
<script>/* This script tag will be replaced with scripts */</script>
<script src="/pageExtensionApi.js"></script>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.jsx" type="module"></script>
</body>
</html>

View File

@ -3,6 +3,7 @@
"name": "dashboard",
"version": "0.1.0",
"private": true,
"type": "module",
"proxy": "http://localhost:8800",
"dependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
@ -37,17 +38,14 @@
"react-i18next": "^13.5.0",
"react-mcjsonchat": "^1.0.0",
"react-router-dom": "6",
"react-scripts": "5.0.1",
"sass": "^1.69.5",
"source-map-explorer": "^2.5.2",
"swagger-ui": "^5.10.3",
"web-vitals": "^3.0.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"start": "vite",
"build": "vite build",
"analyze": "source-map-explorer 'build/static/js/*.js'"
},
"eslintConfig": {
@ -67,5 +65,9 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.0",
"vite": "^5.0.0"
}
}

Some files were not shown because too many files have changed in this diff Show More