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. We call this global exporting.

Conversely, if a script running in virtual-script needs access to a global variable from the main window, we can import this global.

A syntax like the following will be provided:

<virtual-script exported-globals="foo"></virtual-script>
<virtual-script imported-globals="bar"></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.

Supported Attributes

crossorigin: indicates that the browser should provide more information to window.onerror when handling CORS errors

defer: indicates that the script should be executed after the document has parsed (has no effect if src is absent)

integrity: enables the browser to verify that a fetched resource is delivered without unexpected manipulation

nomodule: the script should not be executed in browsers that support ES2015 modules

nonce: user provided nonce values for CSP will be supported

referrerpolicy: indicates which referrer to send when fetching the script

src: the address of the resource

async

TBD describe asynchronous nature of virtual-script.

Unsupported Attributes

charset: this attribute is deprecated

language: this attribute is deprecated

nonce: has special semantics since it is a global attribute and browsers do special behavior with this attribute when a CSP policy is applied. We will have to treat this attribute specially.

Supported Events

Initially we aim to support load and error events. We may add support for more as new use cases arise.

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

We will document the availability of virtual-script tags for use in certain products, and the element will be available for use by the general public. It is important to note that usage of virtual script containers require above-average scrutiny because they bypass the standard component encapsulation model. In addition, it is impossible to test every 3rd party integration, so uses with a new library will require additional rigor.

How we teach this

Unresolved questions

undefined