Skip to content
Open
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 @@ -69,6 +69,7 @@ private static final class JSAnnotationVisitor

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

private LinkedHashSet<String> target;
private LinkedHashSet<String> targetDevelopmentOnly;
Expand All @@ -91,10 +92,22 @@ 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) {
// type=MODULE values are loaded at runtime as <script
// type="module">
// by UIInternals; they must not enter the bundle.
if (currentModule != null && !currentTypeIsModule) {
// This visitor is called also for the $Container annotation
if (currentDevOnly) {
targetDevelopmentOnly.add(currentModule);
Expand All @@ -104,6 +117,7 @@ public void visitEnd() {
}
currentModule = null;
currentDevOnly = false;
currentTypeIsModule = false;
}

}
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,16 @@ void routedComponent_entryPointsAreCollected() {
DepsTests.assertImports(dependencies.getScripts(), "bar.js");
}

@Test
void javaScriptWithTypeModule_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 appShellConfigurator_collectedAsEntryPoint()
throws ClassNotFoundException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.router.Route;

@Route("runtime-js")
@JavaScript("bundled.js")
@JavaScript(value = "module-runtime.js", type = JavaScript.Type.MODULE)
public class RuntimeJavaScriptComponent extends Component {
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,27 @@
@Repeatable(JavaScript.Container.class)
public @interface JavaScript {

/**
* The kind of {@code <script>} tag to render for the dependency.
*/
enum Type {
/**
* Render a classic {@code <script>} tag. Functions declared in the
* loaded file become available in the global scope.
*/
SCRIPT,
/**
* Render a {@code <script type="module">} tag. The loaded file is
* treated as an ES module: functions and variables declared in it are
* private to the module unless explicitly exported. The file is loaded
* at runtime and is not bundled, even when the URL is a bare relative
* path. Use this for hand-authored or CDN-hosted modules that should
* not go through Vite. For build-time bundled ES modules use
* {@link JsModule} instead.
*/
MODULE
}

/**
* JavaScript file URL to load before using the annotated {@link Component}
* in the browser.
Expand All @@ -99,11 +120,28 @@
* frontend directory. Such URLs are not bundled but included into the page
* as standalone scripts in the same way as it's done by
* {@link Page#addJavaScript(String)}.
* <p>
* When {@link #type()} is {@link Type#MODULE}, the value is loaded at
* runtime regardless of whether it has a URL prefix; bare relative paths
* are normalized to {@code context://<value>} and served as static
* resources by the servlet container.
*
* @return a JavaScript file URL
*/
String value();

/**
* The kind of {@code <script>} tag to use when loading the file. Defaults
* to {@link Type#SCRIPT} (a classic {@code <script>} element). Set to
* {@link Type#MODULE} to render a {@code <script type="module">} element
* instead, e.g. for hand-authored or CDN-hosted ES modules that should not
* go through Vite. For build-time bundled ES modules use {@link JsModule}
* instead.
*
* @return the kind of script tag to render
*/
Type type() default Type.SCRIPT;

/**
* Defines if the JavaScript should be loaded only when running in
* development mode (for development tooling etc.) or if it should always be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
import com.vaadin.flow.server.communication.PushConnection;
import com.vaadin.flow.shared.Registration;
import com.vaadin.flow.shared.communication.PushMode;
import com.vaadin.flow.shared.ui.LoadMode;
import com.vaadin.flow.signals.Signal;
import com.vaadin.flow.signals.local.ValueSignal;

Expand Down Expand Up @@ -1191,12 +1192,24 @@ private void maybeWarnAboutDependencies(

private void addExternalDependencies(DependencyInfo dependency) {
Page page = ui.getPage();
dependency.getJavaScripts().stream()
.filter(js -> UrlUtil.isExternal(js.value()))
.forEach(js -> page.addJavaScript(js.value(), js.loadMode()));
dependency.getJavaScripts().stream().filter(this::isRuntimeJavaScript)
.forEach(js -> {
String resolved = FrontendDependencyUrlResolver
.resolveToContextRoot(js.value());
if (resolved == null) {
return;
}
page.addJavaScript(resolved, js.loadMode(), js.type());
});
dependency.getJsModules().stream()
.filter(js -> UrlUtil.isExternal(js.value()))
.forEach(js -> page.addJsModule(js.value()));
.forEach(js -> page.addJavaScript(js.value(), LoadMode.EAGER,
JavaScript.Type.MODULE));
}

private boolean isRuntimeJavaScript(JavaScript js) {
return js.type() == JavaScript.Type.MODULE
|| UrlUtil.isExternal(js.value());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,42 @@ public void addJavaScript(String url) {
* details
*/
public void addJavaScript(String url, LoadMode loadMode) {
addDependency(new Dependency(Type.JAVASCRIPT, url, loadMode));
addJavaScript(url, loadMode, JavaScript.Type.SCRIPT);
}

/**
* Adds the given JavaScript to the page and ensures that it is loaded
* successfully.
* <p>
* Relative URLs are interpreted as relative to the static web resources
* directory. You can prefix the URL with {@code context://} to make it
* relative to the context path or use an absolute URL to refer to files
* outside the frontend directory.
* <p>
* The {@code type} parameter selects the kind of {@code <script>} tag the
* browser receives: {@link JavaScript.Type#SCRIPT} renders a classic
* {@code <script>} element (the default of {@link #addJavaScript(String)});
* {@link JavaScript.Type#MODULE} renders a {@code <script type="module">}
* element, which is the recommended way to load runtime ES modules
* (replaces the deprecated {@link #addJsModule(String)}).
* <p>
* For component related JavaScript dependencies, you should use the
* {@link JavaScript @JavaScript} annotation.
*
* @param url
* the URL to load the JavaScript from, not <code>null</code>
* @param loadMode
* determines dependency load mode, refer to {@link LoadMode} for
* details
* @param type
* the kind of {@code <script>} tag to render; {@code null} is
* treated as {@link JavaScript.Type#SCRIPT}
*/
public void addJavaScript(String url, LoadMode loadMode,
JavaScript.Type type) {
Type dependencyType = type == JavaScript.Type.MODULE ? Type.JS_MODULE
: Type.JAVASCRIPT;
addDependency(new Dependency(dependencyType, url, loadMode));
}

/**
Expand All @@ -282,7 +317,11 @@ public void addJavaScript(String url, LoadMode loadMode) {
* @param url
* the URL to load the JavaScript module from, not
* <code>null</code>
* @deprecated use {@link #addJavaScript(String, LoadMode, JavaScript.Type)}
* with {@link JavaScript.Type#MODULE} instead. The new overload
* also accepts a {@link LoadMode}.
*/
@Deprecated
public void addJsModule(String url) {
if (UrlUtil.isExternal(url) || url.startsWith("/")) {
addDependency(new Dependency(Type.JS_MODULE, url, LoadMode.EAGER));
Expand Down
Loading