Skip to content
Draft
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 @@ -36,9 +36,17 @@
/**
* Copies all frontend resources from JAR files into a given folder.
* <p>
* The task considers "frontend resources" all files placed in
* {@literal META-INF/frontend}, {@literal META-INF/resources/frontend} and
* {@literal META-INF/resources/[**]/themes} folders.
* "Frontend resources" are bundle sources for {@code @JsModule} /
* {@code @CssImport} annotations. The recommended location for them in addon
* JARs is {@literal META-INF/frontend}. The legacy location
* {@literal META-INF/resources/frontend} is still scanned for backwards
* compatibility but is deprecated; a per-jar warning is emitted when it is
* used. Theme files under {@literal META-INF/resources/[**]/themes} are also
* copied.
* <p>
* Public runtime resources for {@code @StyleSheet} / {@code @JavaScript} should
* be placed under {@literal META-INF/resources/} and are served directly by the
* servlet container — they are not handled by this task.
* <p>
* For internal use only. May be renamed or removed in a future release.
*
Expand All @@ -49,6 +57,7 @@ public class TaskCopyFrontendFiles
private static final String WILDCARD_INCLUSION_APP_THEME_JAR = "**/themes/**/*";
private final Options options;
private final Set<File> resourceLocations;
private final Set<File> warnedLegacyLocations = new HashSet<>();

/**
* Scans the jar files given defined by {@code resourcesToScan}.
Expand Down Expand Up @@ -85,10 +94,13 @@ public void execute() {
.addAll(TaskCopyLocalFrontendFiles.copyLocalResources(
new File(location, RESOURCES_FRONTEND_DEFAULT),
targetDirectory));
File legacyDir = new File(location,
COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT);
if (legacyDir.isDirectory()) {
warnAboutDeprecatedFrontendLayout(location);
}
handledFiles.addAll(TaskCopyLocalFrontendFiles
.copyLocalResources(new File(location,
COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT),
targetDirectory));
.copyLocalResources(legacyDir, targetDirectory));
// copies from resources, but excludes already copied from
// resources/frontend
handledFiles
Expand All @@ -100,6 +112,10 @@ public void execute() {
.copyIncludedFilesFromJarTrimmingBasePath(location,
RESOURCES_FRONTEND_DEFAULT, targetDirectory,
"**/*"));
if (jarContentsManager.containsPath(location,
COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT + "/")) {
warnAboutDeprecatedFrontendLayout(location);
}
handledFiles.addAll(jarContentsManager
.copyIncludedFilesFromJarTrimmingBasePath(location,
COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT,
Expand Down Expand Up @@ -136,4 +152,16 @@ private Logger log() {
return LoggerFactory.getLogger(this.getClass());
}

private void warnAboutDeprecatedFrontendLayout(File location) {
if (warnedLegacyLocations.add(location)) {
log().warn("Addon '{}' contains frontend sources under {}/. "
+ "This location is deprecated; migrate them to {}/ "
+ "(bundle sources for @JsModule/@CssImport) or to "
+ "META-INF/resources/ (runtime resources for "
+ "@StyleSheet/@JavaScript).", location.getName(),
COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT,
RESOURCES_FRONTEND_DEFAULT);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ public class ClassInfo {
final LinkedHashSet<String> modulesDevelopmentOnly = new LinkedHashSet<>();
final LinkedHashSet<String> scripts = new LinkedHashSet<>();
final LinkedHashSet<String> scriptsDevelopmentOnly = new LinkedHashSet<>();
/**
* {@code @JsModule} values with a runtime URL prefix ({@code context://},
* {@code base://}, {@code http(s)://}, {@code //}, {@code /}). These are
* filtered out of the bundle and emit a build-time deprecation warning
* recommending migration to {@code @JavaScript}.
*/
final LinkedHashSet<String> deprecatedRuntimeModules = new LinkedHashSet<>();
final transient List<CssData> css = new ArrayList<>();
String route = "";
String layout;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.vaadin.flow.component.dependency.JavaScript;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.FrontendDependencyUrlResolver;
import com.vaadin.flow.theme.NoTheme;
import com.vaadin.flow.theme.Theme;

Expand Down Expand Up @@ -69,14 +70,21 @@ private static final class JSAnnotationVisitor

boolean currentDevOnly = false;
private String currentModule;
private boolean currentTypeIsModule = false;

private LinkedHashSet<String> target;
private LinkedHashSet<String> targetDevelopmentOnly;
private final LinkedHashSet<String> target;
private final LinkedHashSet<String> targetDevelopmentOnly;
private final LinkedHashSet<String> deprecatedRuntimeTarget;
private final boolean isJavaScriptAnnotation;

public JSAnnotationVisitor(LinkedHashSet<String> target,
LinkedHashSet<String> targetDevelopmentOnly) {
LinkedHashSet<String> targetDevelopmentOnly,
boolean isJavaScriptAnnotation,
LinkedHashSet<String> deprecatedRuntimeTarget) {
this.target = target;
this.targetDevelopmentOnly = targetDevelopmentOnly;
this.isJavaScriptAnnotation = isJavaScriptAnnotation;
this.deprecatedRuntimeTarget = deprecatedRuntimeTarget;
}

@Override
Expand All @@ -91,19 +99,43 @@ public void visit(String name, Object value) {
}
}

@Override
public void visitEnum(String name, String descriptor, String value) {
// The "type" attribute only exists on @JavaScript; @JsModule has
// no such attribute, so this method is a no-op for it.
if ("type".equals(name) && "MODULE".equals(value)) {
currentTypeIsModule = true;
}
}

@Override
public void visitEnd() {
super.visitEnd();
if (currentModule != null) {
// This visitor is called also for the $Container annotation
if (currentDevOnly) {
if (currentModule != null && !currentTypeIsModule) {
// type=MODULE @JavaScript values are loaded at runtime as
// <script type="module"> by UIInternals; skip them entirely.
// This visitor is called also for the $Container annotation.
boolean runtimeUrl = FrontendDependencyUrlResolver
.isRuntimeDependencyUrl(currentModule);
if (isJavaScriptAnnotation && runtimeUrl) {
// @JavaScript with a runtime prefix: load at runtime, not
// bundled. No deprecation — this is the recommended form.
} else if (!isJavaScriptAnnotation && runtimeUrl) {
// @JsModule with a runtime URL: keep working at runtime
// via Page.addJsModule, but skip from the bundle and warn
// users to migrate to @JavaScript.
if (deprecatedRuntimeTarget != null) {
deprecatedRuntimeTarget.add(currentModule);
}
} else if (currentDevOnly) {
targetDevelopmentOnly.add(currentModule);
} else {
target.add(currentModule);
}
}
currentModule = null;
currentDevOnly = false;
currentTypeIsModule = false;
}

}
Expand Down Expand Up @@ -226,10 +258,11 @@ public void visit(String name, Object value) {
};
// Visitor for @JsModule annotations
jsModuleVisitor = new JSAnnotationVisitor(classInfo.modules,
classInfo.modulesDevelopmentOnly);
classInfo.modulesDevelopmentOnly, false,
classInfo.deprecatedRuntimeModules);
// Visitor for @JavaScript annotations
jScriptVisitor = new JSAnnotationVisitor(classInfo.scripts,
classInfo.scriptsDevelopmentOnly);
classInfo.scriptsDevelopmentOnly, true, null);
// Visitor all other annotations
annotationVisitor = new RepeatedAnnotationVisitor() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ public FrontendDependencies(ClassFinder finder,
computePackages();
computePwaConfiguration();
aggregateEntryPointInformation();
warnAboutDeprecatedJavaScriptUses();
long ms = (System.nanoTime() - start) / 1000000;
log().info("Visited {} classes. Took {} ms.", visitedClasses.size(),
ms);
Expand Down Expand Up @@ -205,6 +206,43 @@ private void aggregateEntryPointInformation() {

}

private void warnAboutDeprecatedJavaScriptUses() {
Set<String> warnedJavaScriptKeys = new LinkedHashSet<>();
Set<String> warnedJsModuleKeys = new LinkedHashSet<>();
for (Entry<String, ClassInfo> entry : visitedClasses.entrySet()) {
String className = entry.getKey();
ClassInfo classInfo = entry.getValue();
if (classInfo == null) {
continue;
}
for (String value : classInfo.scripts) {
if (warnedJavaScriptKeys.add(className + ':' + value)) {
log().warn(
"@JavaScript on {} with value \"{}\" uses the deprecated bundled interpretation. "
+ "Prepend context:// for a runtime <script> tag, set type=Type.MODULE for a runtime <script type=\"module\"> tag, or migrate to @JsModule for bundling.",
className, value);
}
}
for (String value : classInfo.scriptsDevelopmentOnly) {
if (warnedJavaScriptKeys.add(className + ':' + value)) {
log().warn(
"@JavaScript on {} with value \"{}\" uses the deprecated bundled interpretation. "
+ "Prepend context:// for a runtime <script> tag, set type=Type.MODULE for a runtime <script type=\"module\"> tag, or migrate to @JsModule for bundling.",
className, value);
}
}
for (String value : classInfo.deprecatedRuntimeModules) {
if (warnedJsModuleKeys.add(className + ':' + value)) {
log().warn(
"@JsModule on {} with value \"{}\" is a runtime URL. "
+ "@JsModule is for build-time bundle sources only; "
+ "use @JavaScript(value=\"{}\", type=Type.MODULE) for a runtime <script type=\"module\"> tag.",
className, value, value);
}
}
}
}

Set<String> collectReachableClasses(EntryPointData entryPointData) {
Set<String> classes = new LinkedHashSet<>();
collectReachableClasses(entryPointData.getName(), classes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import com.vaadin.flow.server.frontend.scanner.samples.MyUIInitListener;
import com.vaadin.flow.server.frontend.scanner.samples.RouteComponent;
import com.vaadin.flow.server.frontend.scanner.samples.RouteComponentWithMethodReference;
import com.vaadin.flow.server.frontend.scanner.samples.RuntimeJavaScriptComponent;
import com.vaadin.flow.theme.AbstractTheme;
import com.vaadin.flow.theme.Theme;
import com.vaadin.flow.theme.ThemeDefinition;
Expand Down Expand Up @@ -98,6 +99,27 @@ void routedComponent_entryPointsAreCollected() {
DepsTests.assertImports(dependencies.getScripts(), "bar.js");
}

@Test
void javaScriptWithRuntimePrefixOrTypeModule_excludedFromBundleImports() {
Mockito.when(classFinder.getAnnotatedClasses(Route.class)).thenReturn(
Collections.singleton(RuntimeJavaScriptComponent.class));
FrontendDependencies dependencies = new FrontendDependencies(
classFinder, false, null, true);

DepsTests.assertImports(dependencies.getScripts(), "bundled.js");
}

@Test
void jsModuleWithRuntimeUrl_excludedFromBundleImports() {
Mockito.when(classFinder.getAnnotatedClasses(Route.class)).thenReturn(
Collections.singleton(RuntimeJavaScriptComponent.class));
FrontendDependencies dependencies = new FrontendDependencies(
classFinder, false, null, true);

DepsTests.assertImportsExcludingUI(dependencies.getModules(),
"./bundled-module.js");
}

@Test
void appShellConfigurator_collectedAsEntryPoint()
throws ClassNotFoundException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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.server.frontend.scanner.samples;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.dependency.JavaScript;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.router.Route;

@Route("runtime-js")
@JavaScript("bundled.js")
@JavaScript("context://runtime.js")
@JavaScript("/absolute.js")
@JavaScript("base://servlet-relative.js")
@JavaScript(value = "module-runtime.js", type = JavaScript.Type.MODULE)
@JsModule("./bundled-module.js")
@JsModule("https://cdn.example.com/external-module.js")
@JsModule("context://runtime-module.js")
public class RuntimeJavaScriptComponent extends Component {
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,20 @@
/**
* Imports a CSS file into the application bundle.
* <p>
* The CSS files should be located in the place as JS module files:
* This is a <em>build-time</em> dependency: the referenced CSS file is a bundle
* source processed by Vite at build time and is not served as a static resource
* at runtime. Use {@link StyleSheet} when the CSS file should be served as a
* plain {@code <link rel="stylesheet">} at runtime.
* <p>
* Source locations (same as {@link JsModule}):
* <ul>
* <li>inside {@code frontend} directory in your root project folder in case of
* WAR project
* <li>inside {@code META-INF/resources/frontend} directory (inside a project
* resources folder) in case of JAR project (if you are using Maven this is
* {@code src/main/resources/META-INF/resources/frontend} directory).
* <li>Application projects: {@code src/main/frontend/} (recommended), or the
* legacy top-level {@code frontend/} directory.</li>
* <li>Add-on JARs: {@code META-INF/frontend/} (recommended). The legacy
* location {@code META-INF/resources/frontend/} is still supported but
* deprecated and emits a build-time warning.</li>
* </ul>
* <p>
* The annotation doesn't have any effect in the compatibility mode: use it only
* for Polymer 3 templates.
* <p>
* Depending on the attributes provided, the CSS content will be appended in
* different ways:
*
Expand Down Expand Up @@ -104,6 +106,7 @@
* @since 2.0
*
* @see JsModule
* @see StyleSheet
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
Expand All @@ -112,7 +115,12 @@
@Repeatable(CssImport.Container.class)
public @interface CssImport {
/**
* Location of the file with the CSS content.
* Bundler import specifier for the CSS file. Same rules as
* {@link JsModule#value()} — typically a relative path
* ({@code "./styles/foo.css"}) or a package specifier. Not a URL:
* {@code context://}, {@code base://}, and absolute URLs are not supported
* here. Use {@link StyleSheet} for files that should be served as runtime
* stylesheets.
*
* @return the value.
*/
Expand Down
Loading
Loading