Closed vlukashov closed 3 weeks ago
Workaround (by @tomivirkki
):
We had the same issue = Polymer (and the Vaadin components) got imported from two separate bundles: the one from docs-app and the one from Vaadin (Embedded views)
This is resolved by excluding all Web Components / Polymer related imports from the Vaadin bundle: https://github.com/vaadin/docs/blob/master/webpack.config.js#L35-L36
const fileNameOfTheFlowGeneratedMainEntryPoint = require('path').resolve( __dirname, 'target/frontend/generated-flow-imports.js' ); const filteredFileNameOfTheFlowGeneratedMainEntryPoint = fileNameOfTheFlowGeneratedMainEntryPoint + '-filtered.js'; // @ts-ignore module.exports = merge(flowDefaults, { entry: { bundle: filteredFileNameOfTheFlowGeneratedMainEntryPoint }, plugins: [ function(compiler) { compiler.hooks.afterPlugins.tap( 'Filter out external deps', compilation => { const original = fs.readFileSync( fileNameOfTheFlowGeneratedMainEntryPoint, 'utf8' ); // Exclude component imports which are included in the "bundle" module const filtered = original .split('\n') .filter(row => { if (row.startsWith("import '@vaadin")) return false; if (row.startsWith("import '@polymer")) return false; if (!row.startsWith('import')) return false; return true; }) .join('\n'); fs.writeFileSync( filteredFileNameOfTheFlowGeneratedMainEntryPoint, filtered ); } ); } ] });
More context in a slack discussion
Flow embedding currently works in a way that it creates its own
vaadin-export
bundle out of thegenerated-flow-imports
file. In webpack terms that's anentry
, i.e. a root of a separate dependency graph.By design, webpack entries are not expected to be loaded several into the same page - there is no dependency dedup between them. This leads to problems when embedding Vaadin into any app that also uses Polymer, including V15 TS apps.
If I create a V15 TS app that does not use Polymer (i.e. does not use Vaadin components), embedding server-side Vaadin components into it works just fine as described in the embedding docs. However, as soon as I add a
<vaadin-button>
into the client-side bundle, embedding breaks because now Polymer dependencies end up both invaadin-bundle
andvaadin-export
bundles.But that's not all. Apparently, in addition to the npm dependencies conflict that comes with embedding there is also a conflict between the server-side WebComponents bootstrapping code and the AppShell bootstrapping code. Or that's what I would guess from the
Flow.initApplication is not a function
error in the browser logs.
Note, the steps to import a server-side web component do not need to be the same as embedding into a general application (e.g. JSF). To embed a server-side component, a user could import the web component by import 'xxx/server-counter';
or sth similar.
I have reproduced the issues above and several others. Let me first try to list and categorize them.
Flow’s web component export scripts are served using a request handler. Importing that as a remote script (e. g., from a URL) is hard for Fusion TypeScript views:
import '/web-component/server-counter.js';
fails resolution at compile time in TypeScript and webpack, as they both attempt to resolve and load the script//@ts-ignore
and /* webpackIgnore: true */
workarounds for the above applied, importing fails at runtime when the loaded bundle attempts to discover document.currentScript
, whereas with webpack, the consumer view script’s execution context does not provide it<script type="module" src="/web-component/server-counter.js"></script>
to the view template in the render()
does not work because of the undocumented security feature in lit-html, which removes script tags, see: https://github.com/Polymer/lit-html/issues/759As of now, the only way to load the remote web component export bundle in Fusion is to add a <script>
tag to the document, either in the index.html
template, or dynamically using DOM APIs.
As already mentioned above, any duplicate web component dependencies, such as using <vaadin-button>
or Polymer in both embedded and consumer applications, result in a conflict, because there is no deduplication between bundles, and because web components are registered in the global customElements
registry.
Global registry for custom elements is a limitation in the web platform, which is not likely to be relaxed anytime soon. There is a proposal for Scoped Custom Element Registries, with no implementation or shim available yet.
This issue is less specific to Fusion, and likely manifests in Flow applications embedding other Flow applications too.
In addition to errors from web component registration conflicts, there are also errors likely caused by conflicts of declaring and using global Vaadin
namespace in both consumer and embedded application:
Uncaught TypeError: window.Vaadin.Flow.initApplication is not a function
Uncaught TypeError: $wnd.Vaadin.Flow.registerWidgetset is not a function
Following the Creating an Embedded Vaadin Application Tutorial with Fusion application is hard. The tutorial is generic and does not tell about the above issues, and there is no separate Fusion specific article.
Now on to solution ideas. While issues (3) and (4) are more straightforward to address, we have to consider which way to go about (1) and (2). Here are some options:
Example import:
import 'Frontend/generated/web-component/server-counter';
This solves both (1) and (2). Here the import becomes local instead of remote, and web component dependencies are managed in a regular way by the consumer application.
Pros:
Cons:
Webpack has introduced the Module Federation concept, which is targeted at the same use case (Micro-Frontends). This could solve (1) and (2) with the following ideas:
Pros:
Cons:
We could also tackle (1) and (2) as separate issues, with a custom solution for each:
import 'Remote/web-component/server-counter';
As with any custom solutions, there are cons of more development and maintenance effort, as well as compatibility risks.
Thanks for a great summary, @platosha! 🥇
One use case for embedding server-side web components into TypeScript views is hybrid apps. The currently supported hybrid approach is to combine Flow (Java) and Fusion (TypeScript) UIs keeping them in separate routes. Sometimes a more granular combination may be handy: one may want to include an existing server-side Java component into a TypeScript view. This would be the case when an existing Flow-based app is updated to Vaadin 15+ and developers want to keep using existing (Java) components in TypeScript views as well.
In this case the option A (add generated sources for importing exported web components) looks like a good fit. The breakdown of work for the option A is as follows
frontend/generated/web-component/server-counter
file is generated at the build timeWebComponentBootstrapHandler
to work with the new import semanticsserverSideRoutes
route. In both cases there is a web component with content and behavior fully controlled by server-side Java codeHere is how DX would look like: ServerCounter.java:
public class ServerCounter extends Div {
public ServerCounter() {
add(new Label("Server-side counter"));
}
public static class Exporter extends WebComponentExporter<ServerCounter> {
public Exporter() {
super("server-counter");
}
@Override
protected void configureInstance(
WebComponent<ServerCounter> webComponent,
ServerCounter counter) {
}
}
}
main-layout.ts:
import 'Frontend/generated/web-component/server-counter';
render() {
return html`
<server-counter></server-counter>
<slot></slot>
`;
}
It will have the limitation that the embedded server-side components cannot be in a different project - embedding works only within one Vaadin project.
Another use case for embedding server-side web components into TypeScript views is portal-like apps. The current support for portals includes portlet and OSGi support. This has a limitation that all portlets in a portal need to use the same version of Vaadin, and TypeScript views are not supported at all. No support for TypeScript views could be a road block for customers looking to adopt TypeScript views in portlet / OSGi environments.
In this case a variation of the option C (using an own in-house solution for splitting bundles) looks like a good fit. In fact, the portlet / OSGi support is made possible by a custom bundling logic where Polymer and other frontend dependencies are deployed as a single shared bundle and re-used by all portlets in a portal. When designing support for TypeScript views this existing implementation should be taken into account.
Another use case for cross-app embedding: gradual migration from an older Vaadin app to Fusion: want to embed parts of the old app inside the new app, without putting the old and the new app into one project.
One extra task to consider is that currently the web component is generated into target/frontend
folder, should we keep the file there? or change the logic to use the file in the generated/frontend folder.
The component would have some communication API to the consumer view, e.g. is it ready. since the web component is normally loaded asynchronously.
A first rough estimation of the effort is 2 persons 2-4 sprints.
A working example can be found here https://artur.app.fi/embed-fusion-flow/. Code here https://github.com/Artur-/embed-fusion-flow
Proper embedding support has been implemented and documented for Vaadin 24.4
If I follow the Creating an Embedded Vaadin Application Tutorial in order to embed a server-side Vaadin component into a TypeScript Vaadin view, my application does not work as expected:
webcomponents-loader
script and gives code snippets how to do that. These code snippets do not work. Moreover, the entire step of loading WebComponents polyfils is not needed because all browsers supported by Vaadin 15+ do support Web Components natively.Uncaught TypeError: window.Vaadin.Flow.initApplication is not a function
error:ServerCounter.java:
index.html:
main-layout.ts: