From fb025776bdd3712d34c7657c2c67eabd9c8b44e4 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Wed, 18 Dec 2024 07:13:04 +0100 Subject: [PATCH] fix: re-build production bundle if index.html changes (#20729) Stores index.html hash in stats.json and forces production bundle to be re-built if file contents have changed. Changes to index.html do not trigger a dev bundle re-generation since in dev mode the file is served directly from the frontend folder. Fixes #20629 --- .../server/frontend/BundleValidationUtil.java | 40 +++++++- .../src/main/resources/vite.generated.ts | 1 + .../server/frontend/BundleValidationTest.java | 97 ++++++++++++++++++- .../src/main/frontend/index.html | 25 +++++ .../src/main/frontend/index.html | 25 +++++ .../src/main/frontend/index.html | 25 +++++ .../src/main/frontend/index.html | 25 +++++ 7 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 flow-tests/test-express-build/test-parent-theme-in-frontend-prod/src/main/frontend/index.html create mode 100644 flow-tests/test-express-build/test-parent-theme-prod/src/main/frontend/index.html create mode 100644 flow-tests/test-express-build/test-prod-bundle-no-plugin/src/main/frontend/index.html create mode 100644 flow-tests/test-express-build/test-theme-legacy-components-css-prod/src/main/frontend/index.html diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java index ac1542d1fc7..b0cf7420eb3 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java @@ -49,6 +49,8 @@ */ public final class BundleValidationUtil { + private static final String FRONTEND_HASHES_STATS_KEY = "frontendHashes"; + /** * Checks if an application needs a new frontend bundle. * @@ -217,6 +219,20 @@ private static boolean needsBuildInternal(Options options, // are found missing in bundle. return true; } + + // In dev mode index html is served from frontend folder, not from + // dev-bundle, so rebuild is not required for custom content. + if (options.isProductionMode() && BundleValidationUtil + .hasCustomIndexHtml(options, statsJson)) { + UsageStatistics.markAsUsed("flow/rebundle-reason-custom-index-html", + null); + return true; + } + // index.html hash has already been checked, if needed. + // removing it from hashes map to prevent other unnecessary checks + statsJson.getObject(FRONTEND_HASHES_STATS_KEY) + .remove(FrontendUtils.INDEX_HTML); + if (!BundleValidationUtil.frontendImportsFound(statsJson, options, frontendDependencies)) { UsageStatistics.markAsUsed( @@ -648,7 +664,8 @@ public static boolean frontendImportsFound(JsonObject statsJson, FrontendUtils.FRONTEND_FOLDER_ALIAS.length())) .collect(Collectors.toList()); - final JsonObject frontendHashes = statsJson.getObject("frontendHashes"); + final JsonObject frontendHashes = statsJson + .getObject(FRONTEND_HASHES_STATS_KEY); List faultyContent = new ArrayList<>(); for (String jarImport : jarImports) { @@ -696,6 +713,27 @@ public static boolean frontendImportsFound(JsonObject statsJson, return true; } + private static boolean hasCustomIndexHtml(Options options, + JsonObject statsJson) throws IOException { + File indexHtml = new File(options.getFrontendDirectory(), + FrontendUtils.INDEX_HTML); + if (indexHtml.exists()) { + final JsonObject frontendHashes = statsJson + .getObject(FRONTEND_HASHES_STATS_KEY); + String frontendFileContent = FileUtils.readFileToString(indexHtml, + StandardCharsets.UTF_8); + List faultyContent = new ArrayList<>(); + compareFrontendHashes(frontendHashes, faultyContent, + FrontendUtils.INDEX_HTML, frontendFileContent); + if (!faultyContent.isEmpty()) { + logChangedFiles(faultyContent, + "Detected changed content for frontend files:"); + return true; + } + } + return false; + } + private static boolean indexFileAddedOrDeleted(Options options, JsonObject frontendHashes) { Collection indexFiles = Arrays.asList(FrontendUtils.INDEX_TS, diff --git a/flow-server/src/main/resources/vite.generated.ts b/flow-server/src/main/resources/vite.generated.ts index 1a64fa340c4..8a1d1475d3b 100644 --- a/flow-server/src/main/resources/vite.generated.ts +++ b/flow-server/src/main/resources/vite.generated.ts @@ -297,6 +297,7 @@ function statsExtracterPlugin(): PluginOption { const generatedImports = Array.from(generatedImportsSet).sort(); const frontendFiles: Record = {}; + frontendFiles['index.html'] = createHash('sha256').update(customIndexData.replace(/\r\n/g, '\n'), 'utf8').digest('hex'); const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map'#frontendExtraFileExtensions#]; diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/BundleValidationTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/BundleValidationTest.java index 4df52fb2a18..bd18e5b4889 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/BundleValidationTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/BundleValidationTest.java @@ -25,7 +25,6 @@ import org.mockito.Mockito; import com.vaadin.flow.component.page.AppShellConfigurator; -import com.vaadin.flow.di.Lookup; import com.vaadin.flow.server.Constants; import com.vaadin.flow.server.LoadDependenciesOnStartup; import com.vaadin.flow.server.Mode; @@ -44,6 +43,7 @@ import static com.vaadin.flow.server.Constants.DEV_BUNDLE_JAR_PATH; import static com.vaadin.flow.server.Constants.PROD_BUNDLE_JAR_PATH; import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_FRONTEND_DIR; +import static com.vaadin.flow.server.frontend.FrontendUtils.INDEX_HTML; @RunWith(Parameterized.class) public class BundleValidationTest { @@ -1728,6 +1728,101 @@ public void indexTsDeleted_rebuildRequired() throws IOException { needsBuild); } + @Test + public void indexHtmlNotChanged_rebuildNotRequired() throws IOException { + createPackageJsonStub(BLANK_PACKAGE_JSON_WITH_HASH); + + File frontendFolder = temporaryFolder + .newFolder(FrontendUtils.DEFAULT_FRONTEND_DIR); + + File indexHtml = new File(frontendFolder, FrontendUtils.INDEX_HTML); + indexHtml.createNewFile(); + String defaultIndexHtml = new String(TaskGenerateIndexHtml.class + .getResourceAsStream(INDEX_HTML).readAllBytes(), + StandardCharsets.UTF_8); + FileUtils.write(indexHtml, defaultIndexHtml, StandardCharsets.UTF_8); + + JsonObject stats = getBasicStats(); + stats.getObject(FRONTEND_HASHES).put(INDEX_HTML, + BundleValidationUtil.calculateHash(defaultIndexHtml)); + + final FrontendDependenciesScanner depScanner = Mockito + .mock(FrontendDependenciesScanner.class); + + setupFrontendUtilsMock(stats); + + boolean needsBuild = BundleValidationUtil.needsBuild(options, + depScanner, mode); + Assert.assertFalse("Default 'index.html' should not require bundling", + needsBuild); + } + + @Test + public void indexHtmlChanged_productionMode_rebuildRequired() + throws IOException { + Assume.assumeTrue(mode.isProduction()); + createPackageJsonStub(BLANK_PACKAGE_JSON_WITH_HASH); + + File frontendFolder = temporaryFolder + .newFolder(FrontendUtils.DEFAULT_FRONTEND_DIR); + + File indexHtml = new File(frontendFolder, FrontendUtils.INDEX_HTML); + indexHtml.createNewFile(); + String defaultIndexHtml = new String( + getClass().getResourceAsStream(INDEX_HTML).readAllBytes(), + StandardCharsets.UTF_8); + String customIndexHtml = defaultIndexHtml.replace("", + "
custom content
"); + FileUtils.write(indexHtml, customIndexHtml, StandardCharsets.UTF_8); + JsonObject stats = getBasicStats(); + stats.getObject(FRONTEND_HASHES).put(INDEX_HTML, + BundleValidationUtil.calculateHash(defaultIndexHtml)); + + final FrontendDependenciesScanner depScanner = Mockito + .mock(FrontendDependenciesScanner.class); + + setupFrontendUtilsMock(stats); + + boolean needsBuild = BundleValidationUtil.needsBuild(options, + depScanner, mode); + Assert.assertTrue( + "In production mode, custom 'index.html' should require bundling", + needsBuild); + } + + @Test + public void indexHtmlChanged_developmentMode_rebuildNotRequired() + throws IOException { + Assume.assumeFalse(mode.isProduction()); + createPackageJsonStub(BLANK_PACKAGE_JSON_WITH_HASH); + + File frontendFolder = temporaryFolder + .newFolder(FrontendUtils.DEFAULT_FRONTEND_DIR); + + File indexHtml = new File(frontendFolder, FrontendUtils.INDEX_HTML); + indexHtml.createNewFile(); + String defaultIndexHtml = new String( + getClass().getResourceAsStream(INDEX_HTML).readAllBytes(), + StandardCharsets.UTF_8); + String customIndexHtml = defaultIndexHtml.replace("", + "
custom content
"); + FileUtils.write(indexHtml, customIndexHtml, StandardCharsets.UTF_8); + JsonObject stats = getBasicStats(); + stats.getObject(FRONTEND_HASHES).put(INDEX_HTML, + BundleValidationUtil.calculateHash(defaultIndexHtml)); + + final FrontendDependenciesScanner depScanner = Mockito + .mock(FrontendDependenciesScanner.class); + + setupFrontendUtilsMock(stats); + + boolean needsBuild = BundleValidationUtil.needsBuild(options, + depScanner, mode); + Assert.assertFalse( + "In dev mode, custom 'index.html' should not require bundling", + needsBuild); + } + @Test public void standardVaadinComponent_notAddedToProjectAsJar_noRebuildRequired() throws IOException { diff --git a/flow-tests/test-express-build/test-parent-theme-in-frontend-prod/src/main/frontend/index.html b/flow-tests/test-express-build/test-parent-theme-in-frontend-prod/src/main/frontend/index.html new file mode 100644 index 00000000000..87b750d1be0 --- /dev/null +++ b/flow-tests/test-express-build/test-parent-theme-in-frontend-prod/src/main/frontend/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + +
+ + diff --git a/flow-tests/test-express-build/test-parent-theme-prod/src/main/frontend/index.html b/flow-tests/test-express-build/test-parent-theme-prod/src/main/frontend/index.html new file mode 100644 index 00000000000..87b750d1be0 --- /dev/null +++ b/flow-tests/test-express-build/test-parent-theme-prod/src/main/frontend/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + +
+ + diff --git a/flow-tests/test-express-build/test-prod-bundle-no-plugin/src/main/frontend/index.html b/flow-tests/test-express-build/test-prod-bundle-no-plugin/src/main/frontend/index.html new file mode 100644 index 00000000000..87b750d1be0 --- /dev/null +++ b/flow-tests/test-express-build/test-prod-bundle-no-plugin/src/main/frontend/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + +
+ + diff --git a/flow-tests/test-express-build/test-theme-legacy-components-css-prod/src/main/frontend/index.html b/flow-tests/test-express-build/test-theme-legacy-components-css-prod/src/main/frontend/index.html new file mode 100644 index 00000000000..87b750d1be0 --- /dev/null +++ b/flow-tests/test-express-build/test-theme-legacy-components-css-prod/src/main/frontend/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + +
+ +