Virtual Script Container for LWC Apps

Summary

We need a way for application developers to integrate common third-party libraries into their Web Component based apps (specifically, LWC applications). These libraries, such as Google Analytics, are severely hindered by the Shadow DOM which prevents global interaction with any element on the page.

By providing a virtual container for scripts to run, see, and interact with all the Shadow DOM trees as if it were the light DOM, application developers can continue to run their existing integrations until these libraries support Shadow DOM semantics natively.

Basic example

<virtual-script src="//cdn.optimizely.com/js/12345678.js"></virtual-script>

virtual-script is a custom element which acts the same as the script tag, but any code which is evaluated inside this container is able to traverse the entire Shadow DOM tree using light DOM semantics. So if the above script runs document.querySelectorAll('button'), it will return all <button> elements regardless if they are encapsulated inside a ShadowRoot or not.

<virtual-script>
    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
    })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');

    ga('create', 'UA-XXXXX-Y', 'auto');
    ga('send', 'pageview');
</virtual-script>

Inline scripting would also be supported, such as the above example which uses script injection to fetch its required resources.

Motivation

Web component based applications need the ability to integrate with third party libraries. Because web components rely on the Shadow DOM for encapsulation, this development paradigm does not work with libraries who are expecting to interact with the application globally. Before there was component encapsulation via Shadow DOM it was possible for library authors to:

These libraries are typically added as global scripts to the document root rather than deeper integrations into the components themselves. When every component in the application has a ShadowRoot attached to them, these libraries will not work as expected.

Some examples of these libraries are:

The goal of this proposal is to provide a way for application developers to continue to use their existing integrations until library authors support Shadow DOM integration.

Design

A new element called <virtual-script> encapsulates the various mechanisms to evaluate the scripts in a virtual environment, which solves the problem when using synthetic shadow and subsequently, when using the native Shadow DOM. We can implement this solution in two phases:

Phase I: Synthetic Shadow

Many LWC applications use the synthetic-shadow polyfill. This polyfill emulates the native Shadow DOM behavior while still allowing global styles to cascade into the shadow trees. Synthetic Shadow patches many of the DOM APIs that allow you to interact and traverse the DOM tree, (e.g.: document.querySelector and document.querySelectorAll), these APIs will prevent developers from penetrating the Shadow DOM boundaries defined by their LWC components. In this phase, we want to solve the LWC + Synthetic Shadow scenario.

To ensure we can solve the problem while continuing to guarantee the benefits that are provided by the synthetic-shadow polyfill for all other components running on the main window, we are currently proposing the following characteristics for the virtual-script design:

Other considerations for Phase I:

Phase II: Native Shadow

For applications which are using the native shadow, the underlying implementation will be different, a more complicated one. We are currently proposing the following characteristics for the virtual-script design:

Other considerations for Phase II:

Globals

Some scripts define globals which are meant to be used by the rest of the application. Let's take this example of Google Analytics:

<!-- Google Analytics -->
<script>
    window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
    ga('create', 'UA-XXXXX-Y', 'auto');
    ga('send', 'pageview');
</script>
<script async src='https://www.google-analytics.com/analytics.js'></script>
<!-- End Google Analytics -->

If we evaluate the above snippets with <virtual-script> instead, then ga global value will only be available into the sandbox. So ga will not be usable by anyone else in the entire application. We can however, allow for globals to be exposed into the main window. A syntax like the following could be provided:

<virtual-script extra-globals="ga"></virtual-script>

Note: these global values are controlled side-channels between the main window and the sandbox, and should be used with caution. For example, creating a component that depends on ga to be defined, is an anti-pattern.

Known Limitations

Testing

We will want to gather the top 20 common libraries and implement their behavior inside of virtual-script to ensure that we are properly accounting for all these libraries' use cases. This will be our integration test suite.

Drawbacks

Alternatives

Adoption strategy

TBD

How we teach this

Unresolved questions

undefined