Skip to content
Closed
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
37 changes: 23 additions & 14 deletions flow-server/src/main/resources/vite.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from 'vite';

import brotli from 'rollup-plugin-brotli';
import MagicString from 'magic-string';
import checker from 'vite-plugin-checker';
import postcssLit from '#buildFolder#/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js';
import vaadinI18n from '#buildFolder#/plugins/rollup-plugin-vaadin-i18n/rollup-plugin-vaadin-i18n.js';
Expand Down Expand Up @@ -465,24 +466,32 @@ function preserveUsageStats() {
return {
name: 'vaadin:preserve-usage-stats',

// This hook sees every module of the bundle, and a module handed back
// without a sourcemap is dropped from the map of the chunk it ends up in.
// Returning null for the modules that are not rewritten therefore keeps
// the maps the bundler has for them, and the one module that is rewritten
// gets a map of its own.
transform(src: string, id: string) {
if (id.includes('vaadin-usage-statistics')) {
if (src.includes('vaadin-dev-mode:start')) {
const expectedComment = '/*! vaadin-dev-mode:start';
const newSrc = src.replace(DEV_MODE_START_REGEXP, expectedComment);
if (newSrc === src) {
if (!src.includes(expectedComment)) {
console.error('vaadin-dev-mode:start tag not found');
}
} else if (!newSrc.match(DEV_MODE_CODE_REGEXP)) {
console.error('New comment fails to match original regexp');
} else {
return { code: newSrc };
}
if (!id.includes('vaadin-usage-statistics') || !src.includes('vaadin-dev-mode:start')) {
return null;
}

const expectedComment = '/*! vaadin-dev-mode:start';
const magicString = new MagicString(src).replace(DEV_MODE_START_REGEXP, expectedComment);
if (!magicString.hasChanged()) {
if (!src.includes(expectedComment)) {
console.error('vaadin-dev-mode:start tag not found');
}
return null;
}

const code = magicString.toString();
if (!code.match(DEV_MODE_CODE_REGEXP)) {
console.error('New comment fails to match original regexp');
return null;
}

return { code: src };
return { code, map: magicString.generateMap({ hires: true }) };
}
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2000-2026 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.testutil;

import java.util.ArrayList;
import java.util.List;

import org.junit.Assert;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.json.JsonMapper;

/**
* Helpers for the tests that check the sourcemaps a frontend build emits.
* <p>
* A build plugin that rewrites a module or a chunk has to chain its sourcemap
* onto the one the bundler already has, otherwise the emitted map ends up
* without the original sources and the browser cannot map the bundle back to
* them.
*/
public final class SourceMapTestUtil {

private SourceMapTestUtil() {
}

/**
* Asserts that the given sourcemap can be used to map the bundle it belongs
* to back to the files it was built from.
*
* @param name
* the name of the sourcemap, used in the assertion messages
* @param sourceMap
* the contents of the sourcemap file
* @return the original files the sourcemap refers to
*/
public static List<String> assertSourceMapUsable(String name,
String sourceMap) {
JsonNode contents = JsonMapper.shared().readTree(sourceMap);
JsonNode sources = contents.get("sources");
JsonNode sourcesContent = contents.get("sourcesContent");

Assert.assertNotEquals(name + " should have a sourcemap with sources",
0, sources.size());
Assert.assertNotEquals(name + " should have a sourcemap with mappings",
"", contents.get("mappings").asString());
Assert.assertEquals(
name + " should have the contents of every source in its "
+ "sourcemap",
sources.size(), sourcesContent.size());

List<String> sourceNames = new ArrayList<>();
for (int i = 0; i < sources.size(); i++) {
String source = sources.get(i).asString();
Assert.assertNotEquals(
name + " should have a sourcemap referring to the "
+ "original files, was " + source,
"", source.trim());
Assert.assertNotEquals(
name + " should have the contents of " + source
+ " in its sourcemap",
"", sourcesContent.get(i).asString().trim());
sourceNames.add(source.replace('\\', '/'));
}
return sourceNames;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Stands in for the @vaadin/vaadin-usage-statistics package, which a
// Flow-only application does not pull in. The build plugin that keeps the
// comment below in the bundle matches on the file name and on the marker, so
// this file is enough to run the build through that plugin.
/** vaadin-dev-mode:start
(function () {
window.Vaadin = window.Vaadin || {};
window.Vaadin.registrations = window.Vaadin.registrations || [];
})();
vaadin-dev-mode:end **/

// Keeps the module, and with it the comment, out of reach of tree shaking
window.Vaadin = window.Vaadin || {};
window.Vaadin.usageStatisticsStandIn = true;
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
@Route("com.vaadin.flow.frontend.LitView")
@Tag("lit-view")
@JsModule("./views/lit-view.ts")
// Brings the usage statistics stand-in into the bundle, so that the build runs
// the plugin that keeps its vaadin-dev-mode comment, see DevBundleSourceMapsIT
@JsModule("./vaadin-usage-statistics.js")
public class LitView extends LitTemplate {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright 2000-2026 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 java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;

import com.vaadin.flow.server.Constants;
import com.vaadin.flow.testutil.ChromeBrowserTest;
import com.vaadin.flow.testutil.SourceMapTestUtil;

/**
* A build plugin that hands back the code of a module without a sourcemap drops
* that module from the map of the chunk it ends up in. The plugins of the dev
* bundle build see every module of the bundle, so one of them doing that leaves
* the emitted .map files without sources or mappings and the browser cannot map
* the bundle back to the original files.
*
* @see <a href=
* "https://github.com/vaadin/flow/issues/16679">vaadin/flow#16679</a>
*/
public class DevBundleSourceMapsIT extends ChromeBrowserTest {

private static final String LIT_VIEW_SOURCE = "src/main/frontend/views/lit-view.ts";
private static final String USAGE_STATISTICS_SOURCE = "src/main/frontend/vaadin-usage-statistics.js";

@Override
protected String getTestPath() {
return "/view/com.vaadin.flow.frontend.LitView";
}

@Before
public void init() {
// The dev bundle is built when the application is first opened
open();
waitForElementPresent(By.tagName("lit-view"));
}

@Test
public void devBundleSourceMaps_pointToOriginalSources()
throws IOException {
List<String> sources = new ArrayList<>();
List<File> sourceMaps = getDevBundleSourceMaps();
for (File sourceMap : sourceMaps) {
sources.addAll(SourceMapTestUtil.assertSourceMapUsable(
sourceMap.getName(), read(sourceMap)));
}

assertHasSource(sources, LIT_VIEW_SOURCE);
assertHasSource(sources, USAGE_STATISTICS_SOURCE);
}

/**
* The plugin that keeps the usage statistics comment rewrites the module it
* is in, which is the one case where the plugin has to produce a sourcemap
* of its own instead of leaving the module alone.
*/
@Test
public void usageStatisticsComment_isRewrittenInTheBundle()
throws IOException {
File chunk = getDevBundleChunkOf("vaadin-dev-mode:start");
String contents = read(chunk);

Assert.assertTrue(
chunk.getName() + " should have the usage statistics comment "
+ "rewritten so that it is kept in the bundle",
contents.contains("/*! vaadin-dev-mode:start"));
Assert.assertFalse(
chunk.getName() + " should no longer have the original "
+ "usage statistics comment",
contents.contains("/** vaadin-dev-mode:start"));
}

private void assertHasSource(List<String> sources, String source) {
Assert.assertTrue(
"A sourcemap of the dev bundle should refer to " + source
+ ", only found " + sources,
sources.stream().anyMatch(name -> name.endsWith(source)));
}

private List<File> getDevBundleSourceMaps() {
List<File> sourceMaps = List.of(getDevBundleBuildFolder()
.listFiles((dir, name) -> name.endsWith(".js.map")));
Assert.assertFalse(
"The dev bundle should have been built with sourcemaps "
+ "enabled, see vite.config.ts",
sourceMaps.isEmpty());
return sourceMaps;
}

private File getDevBundleChunkOf(String marker) throws IOException {
List<File> chunks = List.of(getDevBundleBuildFolder()
.listFiles((dir, name) -> name.endsWith(".js")));
for (File chunk : chunks) {
if (read(chunk).contains(marker)) {
return chunk;
}
}
throw new AssertionError("No chunk of the dev bundle contains '"
+ marker + "', looked at " + chunks);
}

private File getDevBundleBuildFolder() {
File baseDir = new File(System.getProperty("user.dir", "."));
File buildFolder = new File(baseDir, "target/"
+ Constants.DEV_BUNDLE_LOCATION + "/webapp/VAADIN/build");
Assert.assertTrue("The dev bundle should have been built into "
+ buildFolder.getPath(), buildFolder.isDirectory());
return buildFolder;
}

private String read(File file) throws IOException {
return FileUtils.readFileToString(file, StandardCharsets.UTF_8);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ function addCssToIndex(): PluginOption {
const customConfig: UserConfigFn = (env) => ({
// Here you can add custom Vite parameters
// https://vitejs.dev/config/
plugins: [addCssToIndex()]
plugins: [addCssToIndex()],
build: {
// Emit separate .map files for the dev bundle so that
// DevBundleSourceMapsIT can verify that the build plugins keep them usable
sourcemap: true
}
});

export default overrideVaadinConfig(customConfig);
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@

import org.junit.Assert;
import org.junit.Test;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.json.JsonMapper;

import com.vaadin.flow.testutil.SourceMapTestUtil;

public class SourceMapsIT extends BundleAccess {

Expand Down Expand Up @@ -55,39 +55,15 @@ public void bundleSourceMapsPointToOriginalSources() throws Exception {
// Bundler runtime helpers are emitted without a sourcemap
continue;
}
assertSourceMapUsable(bundle, JsonMapper.shared()
.readTree(download(BUILD_PATH + matcher.group(1))));
SourceMapTestUtil.assertSourceMapUsable(bundle,
download(BUILD_PATH + matcher.group(1)));
checkedBundles++;
}

Assert.assertNotEquals("No bundle with a sourcemap was found", 0,
checkedBundles);
}

private void assertSourceMapUsable(String bundle, JsonNode sourceMap) {
JsonNode sources = sourceMap.get("sources");
JsonNode sourcesContent = sourceMap.get("sourcesContent");
Assert.assertNotEquals(bundle + " should have a sourcemap with sources",
0, sources.size());
Assert.assertNotEquals(
bundle + " should have a sourcemap with mappings", "",
sourceMap.get("mappings").asString());
Assert.assertEquals(
bundle + " should have the contents of every source in its "
+ "sourcemap",
sources.size(), sourcesContent.size());
for (int i = 0; i < sources.size(); i++) {
Assert.assertNotEquals(
bundle + " should have a sourcemap referring to the "
+ "original files, was " + sources.get(i),
"", sources.get(i).asString().trim());
Assert.assertNotEquals(
bundle + " should have the contents of "
+ sources.get(i).asString() + " in its sourcemap",
"", sourcesContent.get(i).asString().trim());
}
}

/**
* Collects the given bundle and the bundles it imports, directly or through
* another bundle. Names that are not served are left out, as not every file
Expand Down
Loading