14+ NPM mode - com.vaadin.flow.component.internal.ComponentMetaData - Front

com.vaadin.flow.component.internal.ComponentMetaData - Frontend dependencies have not been analyzed for ... To make the component's frontend dependencies work, you must ensure the component class is directly referenced through an application entry point such as a class annotated with @Route

How do we handle this if we have dynamically created components in the view (for example, some components are created via Java reflection)?

Hi,
I don’t think I understand the question.
ComponentMetaData is used to collect data in the compatibility mode (mostly for HTML imports).
The title of the post says NPM mode.
Java classes are scanned in the NPM mode to collect data which has no relation to reflection since reflection is about objects creation. The classes for them are already there.

So the question doesn’t give me any context.
Please provide more information what you are trying to achieve.

Hi Denis,
In order to make it clear, let us create a simple project (in npm mode) from the vaadin/start page and change the main layout as follows to explain my situation:

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.PWA;

import java.lang.reflect.InvocationTargetException;

@Route("")
@PWA(name = "Project Base for Vaadin Flow", shortName = "Project Base")
public class MainView extends Div {

    public MainView() {
        try {
            Class<?> kclass = Class.forName("com.vaadin.flow.component.button.Button");
            Component button = (Component) kclass.getDeclaredConstructor(String.class).newInstance("Click me");
            add(button);
        } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

Here, a Buttpn is clreated dynamically.

If you run this, you will get the following warnings and the button will not be displayed properly.

mvn jetty:run

Output:

[qtp1998450406-20]
 WARN com.vaadin.flow.server.DefaultDeploymentConfiguration - 
====================================================================
Vaadin is running in DEBUG MODE.
Add productionMode=true to web.xml to disable debug features.
====================================================================
[qtp1998450406-20]
 WARN com.vaadin.flow.component.internal.ComponentMetaData - Frontend dependencies have not been analyzed for com.vaadin.flow.component.button.Button. To make the component's frontend dependencies work, you must ensure the component class is directly referenced through an application entry point such as a class annotated with @Route.

My question is: Where can I specify that I have a Button component in the view and its frontend dependencies must be taken care of?

Just a wild idea and not a real answer to your question. One way would be to simply somehow suppress this warning and add vaadin-button to the package.json so you guarantee yourself that this “potential dependency” is being packaged into the target. But again, this is quite an ugly hack and makes me wonder, why do you have to go with instantiating components through reflection in you application in the first place?

We have a lot of views with dynamically generated (full or partial) content. I think others also may come across such situation because this is the case with many large enterprise applications.

So, there must be a way to add components for resolving fronend dependencies in some configuration file.

Where should I add it in the package.json? It’s already appearing in the package.json file in the root folder of the project. Also, is there in the target/frontend/package.json file.

package.json

{
  "name": "no-name",
  "license": "UNLICENSED",
  "dependencies": {
    "@polymer/polymer": "3.2.0",
    "@vaadin/flow-deps": "./target/frontend",
    "@vaadin/vaadin-button": "^2.2.0",
    "@webcomponents/webcomponentsjs": "^2.2.10"
  },
  "devDependencies": {
    "webpack": "4.30.0",
    "webpack-cli": "3.3.0",
    "webpack-dev-server": "3.3.0",
    "webpack-babel-multi-target-plugin": "2.1.0",
    "copy-webpack-plugin": "5.0.3",
    "webpack-merge": "4.2.1"
  }
}

target/frontend/package.json

{
  "name": "@vaadin/flow-deps",
  "version": "1.0.0",
  "license": "UNLICENSED",
  "dependencies": {
    "@vaadin/vaadin-crud": "1.0.5",
    "@vaadin/vaadin-grid": "5.4.6",
    "@vaadin/vaadin-icons": "4.3.1",
    "@polymer/iron-icons": "3.0.1",
    "@vaadin/vaadin-split-layout": "4.1.1",
    "@vaadin/vaadin-combo-box": "5.0.5",
    "@vaadin/vaadin-cookie-consent": "1.1.1",
    "@vaadin/vaadin-core-shrinkwrap": "14.0.0-rc4",
    "@vaadin/vaadin-upload": "4.2.2",
    "@vaadin/vaadin-dialog": "2.2.1",
    "@vaadin/vaadin-select": "2.1.5",
    "@vaadin/vaadin-app-layout": "2.0.2",
    "@polymer/paper-icon-button": "3.0.2",
    "@vaadin/vaadin-item": "2.1.0",
    "@vaadin/vaadin-board": "2.1.0",
    "@vaadin/vaadin-notification": "1.4.0",
    "@vaadin/vaadin-charts": "6.2.3",
    "@vaadin/vaadin-grid-pro": "2.0.3",
    "@vaadin/vaadin-progress-bar": "1.1.2",
    "@vaadin/vaadin-shrinkwrap": "14.0.0-rc4",
    "@vaadin/vaadin-ordered-layout": "1.1.0",
    "@vaadin/vaadin-login": "1.0.1",
    "@vaadin/vaadin-button": "2.2.0",
    "@vaadin/vaadin-date-picker": "4.0.2",
    "@vaadin/vaadin-text-field": "2.4.7",
    "@vaadin/vaadin-menu-bar": "1.0.3",
    "@vaadin/vaadin-custom-field": "1.0.6",
    "@vaadin/vaadin-form-layout": "2.1.4",
    "@vaadin/vaadin-accordion": "1.0.1",
    "@vaadin/vaadin-confirm-dialog": "1.1.1",
    "@polymer/iron-list": "3.0.2",
    "@vaadin/vaadin-list-box": "1.1.1",
    "@vaadin/vaadin-checkbox": "2.2.10",
    "@vaadin/vaadin-details": "1.0.1",
    "@polymer/iron-icon": "3.0.1",
    "@vaadin/vaadin-time-picker": "2.0.1",
    "@vaadin/vaadin-context-menu": "4.3.11",
    "@lrnwebcomponents/pdf-browser-viewer": "2.1.0",
    "@vaadin/vaadin-radio-button": "1.2.3",
    "@vaadin/vaadin-tabs": "3.0.4",
    "@vaadin/vaadin-lumo-styles": "1.5.0",
    "@vaadin/vaadin-material-styles": "1.2.3",
    "@vaadin/vaadin-rich-text-editor": "1.0.4"
  }
}

OK, now I got the issue.
The problem is that ComponentMetaData is a bad class for reporting this kind of issues.
ComponentMetaData is used byg compatibility mode to collect the data and should not report anything about
collecting the data in NPM mode since it has no functionality related to NPM mode.

But this is an internal issue and I will report it on my own.

Regarding to your original question: the code scans for all annotated classes but it also scans the code for real class usage.
So yes, if an instance is created via reflection then its metadata won’t be collected and it won’t be used in the resulting bundle JS which is loaded by the client in the end.
I’m not sure is there a good way to workaround this.

I see this as an issue. Could you please create a ticket about this ?
I will try ask someone who is more familar with this.

Here is what I may suggest as a workaround:
generate a class which has

  • either JsModule annotations (like @JsModule("@vaadin/vaadin-button"))
  • or refer the component directly via field

Then you should directly refer to this class from your view (@Route component).
You may use annotation processor for that : scans all your classes for JsModule annotations and produce the resulting class.
Unfortunately there is no a proper way for doing this.

So as summary:

  • in v13 and v14-bower we visit all the classpath to find components, and everything was added to the production bundle in build time.
  • in v14-npm we optimize the bundle so as only the stuff used by the application is in the bundle, reducing a lot the size of it and facilitating PWAs. In the short future, bundle will be even split in fragments per view.

Then and because instead of visiting all the classpath, we do visit routes, the scanner expects regular applications refering components in fields, methods, etc. Thus dynamic UIs are not supported.

In case you use reflection or any approach that prevents the scanner to figure out during the build time which components are referenced, you need to tell flow what are those components via the @JsModule annotation.
I recommend that in you main route, you add all the component imports that your app creates dynamically.

@Route("")
@JsImport("@vaadin/vadin-button")
@JsImport("@vaadin/vaadin-text-field")
...
class MyRoute {
}

As an additional note, when we implement fragments, we plan to put all non-referenced components in a separated fragment, and in the case the application uses one component in that fragment, it will be loaded automatically. That penalizes the client performance by adding a lot of code that might no be used, but still allows that the app does not fail

Alternatively I would look for some software design pattern, perhaps a factory or so, which would eliminate the need to use reflection.

Thank you Denis and Manuel for the response and detailed explanation. JsModule is a good solution for me because we know what Components are created dynamically. (@Manuel: In your example, you misspelled JsModule as JsImport).

Thomas Mx:
Alternatively I would look for some software design pattern, perhaps a factory or so, which would eliminate the need to use reflection.

Yes, I do agree.

However, I mainly started this thread to understand the npm mode in detail because the documentation is still not available fully.

Hi,

I know this is an old thread, but I ran across the problem just yesterday.

Denis Anisimov:
I see this as an issue. Could you please create a ticket about this ?

Is there a ticket regarding this issue? I would like to follow that.

Manuel Carrasco:
Then and because instead of visiting all the classpath, we do visit routes, the scanner expects regular applications refering components in fields, methods, etc. Thus dynamic UIs are not supported.

We use dynamic components all the time, since we’re developing domain driven… So this might be a big factor for us. I’ll give you an example to make my point a bit more clear:

/**
 * Interface that every dynamic component implements.
 */
public interface DynamicComponent extends HasElement {
  ...
  String getId();
  ...
}
/**
 * View Class (extremely simplyfied), which makes use of the dynamic components.
 */
@Route("")
public class MyView extends AppLayout{
  ...
  private Map<String, HasElement> dynamics;
  
  public MyView(@Autowired Collection<DynamicComponent> dynamics) {
    this.dynamics = dynamics.stream().collect(Collectors.toMap(DynamicComponent::getId, Function.identity()));
	...
  }
  ...
}

This is a very basic version of our use of dynamic content. The components are not part of a specific route, since the components are reused on different pages, based on the state of that specific page.
We use filters and qualifiers to further narrow down which components are used in a specific view.

My current workaround is that i annotate all dynamic components with @Route(registerAtStartup = false). But to me that seems a bit odd and hides actual information. My suggestion would be to have a second annotation to ensure the dynamic content to be scanned. Maybe even design it as a Spring-style meta-annotation if that’s an option.

This should not be an issue anymore.
In the dev mode the “optimization” which scans an entry points (like route targets) is not enabled.
We are using by default the full classpath scanning which should discover all the annotations and should work without any additional workaround or whatever for dynamic components (it should just work in any case).

In production mode we still use “optimization” (byte code scanner) but there is a fallback mechanism which should dynamically load (transparently for the developer and user) missing bundle if byte code scanner was not able to detect dependencies.

So in any way the latest Flow version should work just out of the box in any case: with dynamic components or not.
Alternatively you may just use full class path scanning strategy (not optimized) even for production mode.
There should be configuration property for that.

Denis Anisimov:
This should not be an issue anymore.

Well, it still happens in a freshly set up Spring Boot app (Vaadin 14.0.12, Spring Boot 2.1.7.RELEASE) with Spring classpath-scanning enabled (@SpringBootApplication). I’d guess it happens since there is no Vaadin Annotation (apart from @UIScope) on my classes. The packages are scanned though, my classes are annotated as Spring components (@Component @UIScope) and wired into my view class.

If it happens then please create a ticket.
Please provide detailed info in the ticket.
If issue exists then it’s quite specific to Spring and we will need all details to be able to reproduce this.

I created https://github.com/vaadin/flow/issues/6850