Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: re-build production bundle if index.html changes (#20729) (CP: 24.4) #20738

Merged
merged 1 commit into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -647,7 +663,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 @@ -695,6 +712,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'];

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>