Skip to content

Commit fe150d1

Browse files
fix: re-build production bundle if index.html changes (#20729) (#20737)
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 Co-authored-by: Marco Collovati <marco@vaadin.com>
1 parent bcf2ed2 commit fe150d1

File tree

7 files changed

+236
-2
lines changed

7 files changed

+236
-2
lines changed

flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
*/
5050
public final class BundleValidationUtil {
5151

52+
private static final String FRONTEND_HASHES_STATS_KEY = "frontendHashes";
53+
5254
/**
5355
* Checks if an application needs a new frontend bundle.
5456
*
@@ -217,6 +219,20 @@ private static boolean needsBuildInternal(Options options,
217219
// are found missing in bundle.
218220
return true;
219221
}
222+
223+
// In dev mode index html is served from frontend folder, not from
224+
// dev-bundle, so rebuild is not required for custom content.
225+
if (options.isProductionMode() && BundleValidationUtil
226+
.hasCustomIndexHtml(options, statsJson)) {
227+
UsageStatistics.markAsUsed("flow/rebundle-reason-custom-index-html",
228+
null);
229+
return true;
230+
}
231+
// index.html hash has already been checked, if needed.
232+
// removing it from hashes map to prevent other unnecessary checks
233+
statsJson.getObject(FRONTEND_HASHES_STATS_KEY)
234+
.remove(FrontendUtils.INDEX_HTML);
235+
220236
if (!BundleValidationUtil.frontendImportsFound(statsJson, options,
221237
frontendDependencies)) {
222238
UsageStatistics.markAsUsed(
@@ -647,7 +663,8 @@ public static boolean frontendImportsFound(JsonObject statsJson,
647663
FrontendUtils.FRONTEND_FOLDER_ALIAS.length()))
648664
.collect(Collectors.toList());
649665

650-
final JsonObject frontendHashes = statsJson.getObject("frontendHashes");
666+
final JsonObject frontendHashes = statsJson
667+
.getObject(FRONTEND_HASHES_STATS_KEY);
651668
List<String> faultyContent = new ArrayList<>();
652669

653670
for (String jarImport : jarImports) {
@@ -695,6 +712,27 @@ public static boolean frontendImportsFound(JsonObject statsJson,
695712
return true;
696713
}
697714

715+
private static boolean hasCustomIndexHtml(Options options,
716+
JsonObject statsJson) throws IOException {
717+
File indexHtml = new File(options.getFrontendDirectory(),
718+
FrontendUtils.INDEX_HTML);
719+
if (indexHtml.exists()) {
720+
final JsonObject frontendHashes = statsJson
721+
.getObject(FRONTEND_HASHES_STATS_KEY);
722+
String frontendFileContent = FileUtils.readFileToString(indexHtml,
723+
StandardCharsets.UTF_8);
724+
List<String> faultyContent = new ArrayList<>();
725+
compareFrontendHashes(frontendHashes, faultyContent,
726+
FrontendUtils.INDEX_HTML, frontendFileContent);
727+
if (!faultyContent.isEmpty()) {
728+
logChangedFiles(faultyContent,
729+
"Detected changed content for frontend files:");
730+
return true;
731+
}
732+
}
733+
return false;
734+
}
735+
698736
private static boolean indexFileAddedOrDeleted(Options options,
699737
JsonObject frontendHashes) {
700738
Collection<String> indexFiles = Arrays.asList(FrontendUtils.INDEX_TS,

flow-server/src/main/resources/vite.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ function statsExtracterPlugin(): PluginOption {
297297
const generatedImports = Array.from(generatedImportsSet).sort();
298298

299299
const frontendFiles: Record<string, string> = {};
300+
frontendFiles['index.html'] = createHash('sha256').update(customIndexData.replace(/\r\n/g, '\n'), 'utf8').digest('hex');
300301

301302
const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map'];
302303

flow-server/src/test/java/com/vaadin/flow/server/frontend/BundleValidationTest.java

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.mockito.Mockito;
2626

2727
import com.vaadin.flow.component.page.AppShellConfigurator;
28-
import com.vaadin.flow.di.Lookup;
2928
import com.vaadin.flow.server.Constants;
3029
import com.vaadin.flow.server.LoadDependenciesOnStartup;
3130
import com.vaadin.flow.server.Mode;
@@ -44,6 +43,7 @@
4443
import static com.vaadin.flow.server.Constants.DEV_BUNDLE_JAR_PATH;
4544
import static com.vaadin.flow.server.Constants.PROD_BUNDLE_JAR_PATH;
4645
import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_FRONTEND_DIR;
46+
import static com.vaadin.flow.server.frontend.FrontendUtils.INDEX_HTML;
4747

4848
@RunWith(Parameterized.class)
4949
public class BundleValidationTest {
@@ -1728,6 +1728,101 @@ public void indexTsDeleted_rebuildRequired() throws IOException {
17281728
needsBuild);
17291729
}
17301730

1731+
@Test
1732+
public void indexHtmlNotChanged_rebuildNotRequired() throws IOException {
1733+
createPackageJsonStub(BLANK_PACKAGE_JSON_WITH_HASH);
1734+
1735+
File frontendFolder = temporaryFolder
1736+
.newFolder(FrontendUtils.DEFAULT_FRONTEND_DIR);
1737+
1738+
File indexHtml = new File(frontendFolder, FrontendUtils.INDEX_HTML);
1739+
indexHtml.createNewFile();
1740+
String defaultIndexHtml = new String(TaskGenerateIndexHtml.class
1741+
.getResourceAsStream(INDEX_HTML).readAllBytes(),
1742+
StandardCharsets.UTF_8);
1743+
FileUtils.write(indexHtml, defaultIndexHtml, StandardCharsets.UTF_8);
1744+
1745+
JsonObject stats = getBasicStats();
1746+
stats.getObject(FRONTEND_HASHES).put(INDEX_HTML,
1747+
BundleValidationUtil.calculateHash(defaultIndexHtml));
1748+
1749+
final FrontendDependenciesScanner depScanner = Mockito
1750+
.mock(FrontendDependenciesScanner.class);
1751+
1752+
setupFrontendUtilsMock(stats);
1753+
1754+
boolean needsBuild = BundleValidationUtil.needsBuild(options,
1755+
depScanner, mode);
1756+
Assert.assertFalse("Default 'index.html' should not require bundling",
1757+
needsBuild);
1758+
}
1759+
1760+
@Test
1761+
public void indexHtmlChanged_productionMode_rebuildRequired()
1762+
throws IOException {
1763+
Assume.assumeTrue(mode.isProduction());
1764+
createPackageJsonStub(BLANK_PACKAGE_JSON_WITH_HASH);
1765+
1766+
File frontendFolder = temporaryFolder
1767+
.newFolder(FrontendUtils.DEFAULT_FRONTEND_DIR);
1768+
1769+
File indexHtml = new File(frontendFolder, FrontendUtils.INDEX_HTML);
1770+
indexHtml.createNewFile();
1771+
String defaultIndexHtml = new String(
1772+
getClass().getResourceAsStream(INDEX_HTML).readAllBytes(),
1773+
StandardCharsets.UTF_8);
1774+
String customIndexHtml = defaultIndexHtml.replace("<body>",
1775+
"<body><div>custom content</div>");
1776+
FileUtils.write(indexHtml, customIndexHtml, StandardCharsets.UTF_8);
1777+
JsonObject stats = getBasicStats();
1778+
stats.getObject(FRONTEND_HASHES).put(INDEX_HTML,
1779+
BundleValidationUtil.calculateHash(defaultIndexHtml));
1780+
1781+
final FrontendDependenciesScanner depScanner = Mockito
1782+
.mock(FrontendDependenciesScanner.class);
1783+
1784+
setupFrontendUtilsMock(stats);
1785+
1786+
boolean needsBuild = BundleValidationUtil.needsBuild(options,
1787+
depScanner, mode);
1788+
Assert.assertTrue(
1789+
"In production mode, custom 'index.html' should require bundling",
1790+
needsBuild);
1791+
}
1792+
1793+
@Test
1794+
public void indexHtmlChanged_developmentMode_rebuildNotRequired()
1795+
throws IOException {
1796+
Assume.assumeFalse(mode.isProduction());
1797+
createPackageJsonStub(BLANK_PACKAGE_JSON_WITH_HASH);
1798+
1799+
File frontendFolder = temporaryFolder
1800+
.newFolder(FrontendUtils.DEFAULT_FRONTEND_DIR);
1801+
1802+
File indexHtml = new File(frontendFolder, FrontendUtils.INDEX_HTML);
1803+
indexHtml.createNewFile();
1804+
String defaultIndexHtml = new String(
1805+
getClass().getResourceAsStream(INDEX_HTML).readAllBytes(),
1806+
StandardCharsets.UTF_8);
1807+
String customIndexHtml = defaultIndexHtml.replace("<body>",
1808+
"<body><div>custom content</div>");
1809+
FileUtils.write(indexHtml, customIndexHtml, StandardCharsets.UTF_8);
1810+
JsonObject stats = getBasicStats();
1811+
stats.getObject(FRONTEND_HASHES).put(INDEX_HTML,
1812+
BundleValidationUtil.calculateHash(defaultIndexHtml));
1813+
1814+
final FrontendDependenciesScanner depScanner = Mockito
1815+
.mock(FrontendDependenciesScanner.class);
1816+
1817+
setupFrontendUtilsMock(stats);
1818+
1819+
boolean needsBuild = BundleValidationUtil.needsBuild(options,
1820+
depScanner, mode);
1821+
Assert.assertFalse(
1822+
"In dev mode, custom 'index.html' should not require bundling",
1823+
needsBuild);
1824+
}
1825+
17311826
@Test
17321827
public void standardVaadinComponent_notAddedToProjectAsJar_noRebuildRequired()
17331828
throws IOException {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!DOCTYPE html>
2+
<!--
3+
This file is auto-generated by Vaadin.
4+
-->
5+
6+
<!-- default production bundle -->
7+
8+
<html>
9+
<head>
10+
<meta charset="UTF-8" />
11+
<meta name="viewport" content="width=device-width, initial-scale=1" />
12+
<style>
13+
body, #outlet {
14+
height: 100vh;
15+
width: 100%;
16+
margin: 0;
17+
}
18+
</style>
19+
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
20+
</head>
21+
<body>
22+
<!-- This outlet div is where the views are rendered -->
23+
<div id="outlet"></div>
24+
</body>
25+
</html>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!DOCTYPE html>
2+
<!--
3+
This file is auto-generated by Vaadin.
4+
-->
5+
6+
<!-- default production bundle -->
7+
8+
<html>
9+
<head>
10+
<meta charset="UTF-8" />
11+
<meta name="viewport" content="width=device-width, initial-scale=1" />
12+
<style>
13+
body, #outlet {
14+
height: 100vh;
15+
width: 100%;
16+
margin: 0;
17+
}
18+
</style>
19+
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
20+
</head>
21+
<body>
22+
<!-- This outlet div is where the views are rendered -->
23+
<div id="outlet"></div>
24+
</body>
25+
</html>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!DOCTYPE html>
2+
<!--
3+
This file is auto-generated by Vaadin.
4+
-->
5+
6+
<!-- default production bundle -->
7+
8+
<html>
9+
<head>
10+
<meta charset="UTF-8" />
11+
<meta name="viewport" content="width=device-width, initial-scale=1" />
12+
<style>
13+
body, #outlet {
14+
height: 100vh;
15+
width: 100%;
16+
margin: 0;
17+
}
18+
</style>
19+
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
20+
</head>
21+
<body>
22+
<!-- This outlet div is where the views are rendered -->
23+
<div id="outlet"></div>
24+
</body>
25+
</html>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!DOCTYPE html>
2+
<!--
3+
This file is auto-generated by Vaadin.
4+
-->
5+
6+
<!-- default production bundle -->
7+
8+
<html>
9+
<head>
10+
<meta charset="UTF-8" />
11+
<meta name="viewport" content="width=device-width, initial-scale=1" />
12+
<style>
13+
body, #outlet {
14+
height: 100vh;
15+
width: 100%;
16+
margin: 0;
17+
}
18+
</style>
19+
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
20+
</head>
21+
<body>
22+
<!-- This outlet div is where the views are rendered -->
23+
<div id="outlet"></div>
24+
</body>
25+
</html>

0 commit comments

Comments
 (0)