From 8282506c6497c45795ca8852dbf49dbba35f5314 Mon Sep 17 00:00:00 2001 From: Artur Date: Tue, 16 May 2023 15:18:49 +0300 Subject: [PATCH] fix: Store and reuse index.html modifications for dev bundles (#16794) Fixes #16792 --- .../vaadin/flow/server/BootstrapHandler.java | 55 +++++++++---------- .../IndexHtmlRequestHandler.java | 2 +- .../WebComponentBootstrapHandler.java | 2 +- .../src/main/resources/vite.generated.ts | 31 +++++++++-- .../java/com/vaadin/tests/util/TestUtil.java | 3 +- .../main/webapp/imported-by-vite-plugin.css | 3 + .../flow/frontend/ViteImportedCSSIT.java | 50 +++++++++++++++++ .../vite.config.ts | 29 ++++++++++ 8 files changed, 139 insertions(+), 36 deletions(-) create mode 100644 flow-tests/test-express-build/test-dev-bundle-frontend-add-on/src/main/webapp/imported-by-vite-plugin.css create mode 100644 flow-tests/test-express-build/test-dev-bundle-frontend-add-on/src/test/java/com/vaadin/flow/frontend/ViteImportedCSSIT.java create mode 100644 flow-tests/test-express-build/test-dev-bundle-frontend-add-on/vite.config.ts diff --git a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java index 483f0a153d4..d9b22efb56f 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java @@ -25,7 +25,6 @@ import java.io.OutputStreamWriter; import java.io.Serializable; import java.lang.annotation.Annotation; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -49,7 +48,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.commons.io.IOUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.DataNode; import org.jsoup.nodes.Document; @@ -70,6 +68,7 @@ import com.vaadin.flow.function.DeploymentConfiguration; import com.vaadin.flow.internal.AnnotationReader; import com.vaadin.flow.internal.BootstrapHandlerHelper; +import com.vaadin.flow.internal.JsonUtils; import com.vaadin.flow.internal.ReflectTools; import com.vaadin.flow.internal.UsageStatisticsExporter; import com.vaadin.flow.router.InvalidLocationException; @@ -96,6 +95,7 @@ import elemental.json.JsonObject; import elemental.json.JsonValue; import elemental.json.impl.JsonUtil; + import static com.vaadin.flow.server.Constants.VAADIN_MAPPING; import static com.vaadin.flow.server.frontend.FrontendUtils.EXPORT_CHUNK; import static java.nio.charset.StandardCharsets.UTF_8; @@ -1595,17 +1595,15 @@ protected static void setupPwa(Document document, VaadinService service) { setupPwa(document, service.getPwaRegistry()); } - protected static void addJavaScriptEntryPoints( + protected static void addGeneratedIndexContent( DeploymentConfiguration config, Document targetDocument) throws IOException { - URL statsJsonUrl = DevBundleUtils - .findBundleFile(config.getProjectFolder(), "config/stats.json"); - Objects.requireNonNull(statsJsonUrl, + String statsJson = DevBundleUtils + .findBundleStatsJson(config.getProjectFolder()); + Objects.requireNonNull(statsJson, "Frontend development bundle is expected to be in the project" + " or on the classpath, but not found."); - String statsJson = IOUtils.toString(statsJsonUrl, - StandardCharsets.UTF_8); - addEntryScripts(targetDocument, Json.parse(statsJson)); + addGeneratedIndexContent(targetDocument, Json.parse(statsJson)); } /** @@ -1676,31 +1674,30 @@ private static Element getStyleTag(String themeName, String fileName, return element; } - private static void addEntryScripts(Document targetDocument, + private static void addGeneratedIndexContent(Document targetDocument, JsonObject statsJson) { - boolean addIndexHtml = true; - Element indexHtmlScript = null; - JsonArray entryScripts = statsJson.getArray("entryScripts"); - for (int i = 0; i < entryScripts.length(); i++) { - String entryScript = entryScripts.getString(i); - Element elm = new Element(SCRIPT_TAG); - elm.attr("type", "module"); - elm.attr("src", entryScript); - targetDocument.head().appendChild(elm); + JsonArray indexHtmlGeneratedRows = statsJson + .getArray("indexHtmlGenerated"); + List toAdd = new ArrayList<>(); - if (entryScript.contains("indexhtml")) { - indexHtmlScript = elm; - } + Optional webComponentScript = JsonUtils + .stream(statsJson.getArray("entryScripts")) + .map(value -> value.asString()) + .filter(script -> script.contains("webcomponenthtml")) + .findFirst(); - if (entryScript.contains("webcomponenthtml")) { - addIndexHtml = false; - } + if (webComponentScript.isPresent()) { + Element elm = new Element(SCRIPT_TAG); + elm.attr("type", "module"); + elm.attr("src", webComponentScript.get()); + toAdd.add(elm.outerHtml()); + } else { + toAdd.addAll(JsonUtils.stream(indexHtmlGeneratedRows) + .map(value -> value.asString()).toList()); } - // If a reference to webcomponenthtml is present, the embedded - // components are used, thus we don't need to serve indexhtml script - if (!addIndexHtml && indexHtmlScript != null) { - indexHtmlScript.remove(); + for (String row : toAdd) { + targetDocument.head().append(row); } } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java index cb48a2ee488..798fff91e7f 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java @@ -418,7 +418,7 @@ private static Document getIndexHtmlDocument(VaadinService service) // When running without a frontend server, the index.html comes // directly from the frontend folder and the JS entrypoint(s) need // to be added - addJavaScriptEntryPoints(config, indexHtmlDocument); + addGeneratedIndexContent(config, indexHtmlDocument); } modifyIndexHtmlForVite(indexHtmlDocument); return indexHtmlDocument; diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentBootstrapHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentBootstrapHandler.java index 4a9a534c51e..4bc5f1fa56d 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentBootstrapHandler.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentBootstrapHandler.java @@ -135,7 +135,7 @@ public Document getBootstrapPage(BootstrapContext context) { // directly from the frontend folder and the JS // entrypoint(s) need // to be added - addJavaScriptEntryPoints(deploymentConfiguration, document); + addGeneratedIndexContent(deploymentConfiguration, document); } // Specify the application ID for scripts of the diff --git a/flow-server/src/main/resources/vite.generated.ts b/flow-server/src/main/resources/vite.generated.ts index 3580a566ab8..019915acc06 100644 --- a/flow-server/src/main/resources/vite.generated.ts +++ b/flow-server/src/main/resources/vite.generated.ts @@ -55,6 +55,8 @@ const bundleSizeFile = path.resolve(statsFolder, 'bundle-size.html'); const nodeModulesFolder = path.resolve(__dirname, 'node_modules'); const webComponentTags = '#webComponentTags#'; +const projectIndexHtml = path.resolve(frontendFolder, 'index.html'); + const projectStaticAssetsFolders = [ path.resolve(__dirname, 'src', 'main', 'resources', 'META-INF', 'resources'), path.resolve(__dirname, 'src', 'main', 'resources', 'static'), @@ -229,8 +231,11 @@ function statsExtracterPlugin(): PluginOption { .sort() .filter((value, index, self) => self.indexOf(value) === index); const npmModuleAndVersion = Object.fromEntries(npmModules.map((module) => [module, getVersion(module)])); - const cvdls = Object.fromEntries(npmModules.filter((module) => getCvdlName(module) != null) - .map((module) => [module, {name: getCvdlName(module),version: getVersion(module)}])); + const cvdls = Object.fromEntries( + npmModules + .filter((module) => getCvdlName(module) != null) + .map((module) => [module, { name: getCvdlName(module), version: getVersion(module) }]) + ); mkdirSync(path.dirname(statsFile), { recursive: true }); const projectPackageJson = JSON.parse(readFileSync(projectPackageJsonFile, { encoding: 'utf-8' })); @@ -238,6 +243,23 @@ function statsExtracterPlugin(): PluginOption { const entryScripts = Object.values(bundle) .filter((bundle) => bundle.isEntry) .map((bundle) => bundle.fileName); + + const generatedIndexHtml = path.resolve(buildOutputFolder, 'index.html'); + const customIndexData: string = readFileSync(projectIndexHtml, { encoding: 'utf-8' }); + const generatedIndexData: string = readFileSync(generatedIndexHtml, { + encoding: 'utf-8' + }); + + const customIndexRows = new Set(customIndexData.split(/[\r\n]/).filter((row) => row.trim() !== '')); + const generatedIndexRows = generatedIndexData.split(/[\r\n]/).filter((row) => row.trim() !== ''); + + const rowsGenerated: string[] = []; + generatedIndexRows.forEach((row) => { + if (!customIndexRows.has(row)) { + rowsGenerated.push(row); + } + }); + //After dev-bundle build add used Flow frontend imports JsModule/JavaScript/CssImport const parseImports = (filename: string, result: Set): void => { @@ -343,7 +365,8 @@ function statsExtracterPlugin(): PluginOption { entryScripts, webComponents, cvdlModules: cvdls, - packageJsonHash: projectPackageJson?.vaadin?.hash + packageJsonHash: projectPackageJson?.vaadin?.hash, + indexHtmlGenerated: rowsGenerated }; writeFileSync(statsFile, JSON.stringify(stats, null, 1)); } @@ -663,7 +686,7 @@ export const vaadinConfig: UserConfigFn = (env) => { assetsDir: 'VAADIN/build', rollupOptions: { input: { - indexhtml: path.resolve(frontendFolder, 'index.html'), + indexhtml: projectIndexHtml, ...(hasExportedWebComponents ? { webcomponenthtml: path.resolve(frontendFolder, 'web-component.html') } : {}) }, diff --git a/flow-server/src/test/java/com/vaadin/tests/util/TestUtil.java b/flow-server/src/test/java/com/vaadin/tests/util/TestUtil.java index c235525fa1f..ab5262d03fb 100644 --- a/flow-server/src/test/java/com/vaadin/tests/util/TestUtil.java +++ b/flow-server/src/test/java/com/vaadin/tests/util/TestUtil.java @@ -110,7 +110,8 @@ public static void createStatsJsonStub(File projectRootFolder) throws IOException { String content = "{\"npmModules\": {}, " + "\"entryScripts\": [\"foo.js\"], " - + "\"packageJsonHash\": \"42\"}"; + + "\"packageJsonHash\": \"42\"," + + "\"indexHtmlGenerated\": []}"; createStubFile(projectRootFolder, Constants.DEV_BUNDLE_LOCATION + "/config/stats.json", content); } diff --git a/flow-tests/test-express-build/test-dev-bundle-frontend-add-on/src/main/webapp/imported-by-vite-plugin.css b/flow-tests/test-express-build/test-dev-bundle-frontend-add-on/src/main/webapp/imported-by-vite-plugin.css new file mode 100644 index 00000000000..d3d7cc6f852 --- /dev/null +++ b/flow-tests/test-express-build/test-dev-bundle-frontend-add-on/src/main/webapp/imported-by-vite-plugin.css @@ -0,0 +1,3 @@ +body { + background: rgba(173, 216, 230, 1); +} diff --git a/flow-tests/test-express-build/test-dev-bundle-frontend-add-on/src/test/java/com/vaadin/flow/frontend/ViteImportedCSSIT.java b/flow-tests/test-express-build/test-dev-bundle-frontend-add-on/src/test/java/com/vaadin/flow/frontend/ViteImportedCSSIT.java new file mode 100644 index 00000000000..013961ada81 --- /dev/null +++ b/flow-tests/test-express-build/test-dev-bundle-frontend-add-on/src/test/java/com/vaadin/flow/frontend/ViteImportedCSSIT.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2023 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.frontend; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.io.FileUtils; +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.flow.server.Constants; +import com.vaadin.flow.testutil.ChromeBrowserTest; + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonObject; + +public class ViteImportedCSSIT extends ChromeBrowserTest { + + @Override + protected String getTestPath() { + return "/view/"; + } + + @Test + public void cssImportedByVite_availableInApp() throws IOException { + open(); + WebElement body = $("body").first(); + + Assert.assertEquals("rgba(173, 216, 230, 1)", + body.getCssValue("background-color")); + } +} diff --git a/flow-tests/test-express-build/test-dev-bundle-frontend-add-on/vite.config.ts b/flow-tests/test-express-build/test-dev-bundle-frontend-add-on/vite.config.ts new file mode 100644 index 00000000000..a5f1726742b --- /dev/null +++ b/flow-tests/test-express-build/test-dev-bundle-frontend-add-on/vite.config.ts @@ -0,0 +1,29 @@ +import { PluginOption, UserConfigFn } from 'vite'; +import { overrideVaadinConfig } from './vite.generated'; + +function addCssToIndex(): PluginOption { + return { + name: 'generate-css', + transformIndexHtml: (_html, _conf) => { + const tags = [ + { + tag: 'link', + attrs: { + rel: 'stylesheet', + href: '/imported-by-vite-plugin.css' + } + } + ]; + + return tags; + } + }; +} + +const customConfig: UserConfigFn = (env) => ({ + // Here you can add custom Vite parameters + // https://vitejs.dev/config/ + plugins: [addCssToIndex()] +}); + +export default overrideVaadinConfig(customConfig);