Skip to content

Commit

Permalink
fix: re-build production bundle if index.html changes (#20729)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mcollovati authored Dec 18, 2024
1 parent 060b835 commit fb02577
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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<String> faultyContent = new ArrayList<>();

for (String jarImport : jarImports) {
Expand Down Expand Up @@ -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<String> 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<String> indexFiles = Arrays.asList(FrontendUtils.INDEX_TS,
Expand Down
1 change: 1 addition & 0 deletions flow-server/src/main/resources/vite.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ function statsExtracterPlugin(): PluginOption {
const generatedImports = Array.from(generatedImportsSet).sort();

const frontendFiles: Record<string, string> = {};
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#];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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("<body>",
"<body><div>custom content</div>");
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("<body>",
"<body><div>custom content</div>");
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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<!--
This file is auto-generated by Vaadin.
-->

<!-- default production bundle -->

<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body, #outlet {
height: 100vh;
width: 100%;
margin: 0;
}
</style>
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
</head>
<body>
<!-- This outlet div is where the views are rendered -->
<div id="outlet"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<!--
This file is auto-generated by Vaadin.
-->

<!-- default production bundle -->

<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body, #outlet {
height: 100vh;
width: 100%;
margin: 0;
}
</style>
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
</head>
<body>
<!-- This outlet div is where the views are rendered -->
<div id="outlet"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<!--
This file is auto-generated by Vaadin.
-->

<!-- default production bundle -->

<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body, #outlet {
height: 100vh;
width: 100%;
margin: 0;
}
</style>
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
</head>
<body>
<!-- This outlet div is where the views are rendered -->
<div id="outlet"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<!--
This file is auto-generated by Vaadin.
-->

<!-- default production bundle -->

<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body, #outlet {
height: 100vh;
width: 100%;
margin: 0;
}
</style>
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
</head>
<body>
<!-- This outlet div is where the views are rendered -->
<div id="outlet"></div>
</body>
</html>

0 comments on commit fb02577

Please sign in to comment.