Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle inline systemjs module scripts #2357

Closed
wants to merge 1 commit into from
Closed

Conversation

dmail
Copy link
Contributor

@dmail dmail commented Sep 19, 2021

Description

Makes Systemjs executes inline <script type="systemjs-module">

Motivation

I have an html file with inline module script. It's important to keep module script inline in the HTML file so that user sees the "hello world" message right away.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Title</title>
  </head>

  <body>
    <script type="module">
      document.body.appendChild(document.createTextNode('Hello world'))
    </script>
  </body>
</html>

With this PR the following HTML would be executed by Systemjs.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Title</title>
    <script>
    /* Here is an inlined version of s.min.js */
    </script>
  </head>

  <body>
    <script type="systemjs-module">
      document.body.appendChild(document.createTextNode('Hello world'))
    </script>
  </body>
</html>

Alternative

<script type="systemjs-module">
  document.body.appendChild(document.createTextNode('Hello world'))
</script>

Could be replaced by

<script type="systemjs-module" src="inline_script.js"></script>

But it means an additional HTTP request performed by Systemjs to fetch "inline_script.js". This would defeat the reason I inlined the script in the first place: show something to the user immediatly.

See #2148 (comment)

@github-actions
Copy link

File size impact

Merging main into main will impact 3 files in 2 groups.

browser (2/2)
File raw gzip brotli Event
dist/s.min.js +236 (7,797) +80 (3,063) +66 (2,775) modified
dist/system.min.js +236 (12,086) +77 (4,647) +63 (4,183) modified
Total size impact +472 (19,883) +157 (7,710) +129 (6,958)
node (1/1)
File raw gzip brotli Event
dist/system-node.cjs +342 (202,425) +91 (52,358) +34 (43,983) modified
Total size impact +342 (202,425) +91 (52,358) +34 (43,983)
extras (0/8)

No impact on files in extras group.

Generated by file size impact during size-impact#1250649116

Copy link
Collaborator

@joeldenning joeldenning left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After looking through this, my first thought was "why not just use <script type="text/javascript">?" Is it because you wish to be able to import from other modules? I'd like to understand the use case more - the example you shared doesn't call System.register() nor does it import or export anything inside of the inline module, so it's unclear to me why you couldn't just use <script type="text/javascript">.

One thing to note is that with native modules it is not possible to retrieve the exports of inlined modules. For example in the following code, it's not possible to retrieve the a export in any way from any other module:

<script type="module">
export const a = 1;
</script>

It's important to keep module script inline in the HTML file so that user sees the "hello world" message right away.

<script type=module> is always asynchronous, no matter what. So while inlining could have performance benefits by avoiding the network request to download the javascript file, it wouldn't ensure that the javascript is executed before the rest of the DOM content that comes below the <script>. This is not a reason to dismiss the feature entirely, but I just wanted to note it since I think any systemjs implementation should mimic that async behavior.

if (typeof inlineScriptContent === "string") {
if (inlineScriptContent.indexOf("//# sourceURL=") < 0)
inlineScriptContent += "\n//# sourceURL=" + url
;(0, eval)(inlineScriptContent)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand things correctly, your code here requires the user to call System.import('https://example.com/_inline_script_0') in order for the <script type="systemjs-module"></script> to be executed? If so, I don't think it's a good solution for the following reasons:

  1. The _inline_script_0 part is a magic string that would not be obvious for people learning systemjs
  2. Correlating an inline module to a URL is not something that browsers do and, as far as I know, there's no precedent for it.
  3. The user would probably expect the inline module to be executed automatically, and be confused when it isn't and requires them to call System.import('https://example.com/_inline_script_0') manually to execute it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your code here requires the user to call System.import('https://example.com/_inline_script_0')

The code is auto executed when systemjs collects the script tags and calls System.import

System.import(script.src.slice(0, 7) === 'import:' ? script.src.slice(7) : resolveUrl(script.src, baseUrl)).catch(function (e) {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I missed that, makes sense.

@dmail
Copy link
Contributor Author

dmail commented Sep 21, 2021

"why not just use <script type="text/javascript">?"

At first I was using an inline regular script tag. Then I wanted to import a module script from that regular script. So I naturally converted the regular script into a module one. Let me illustrate with code below.

(I have shortened the example below to show the concept, in reality the code doing app.innerHTML = 'something' is a bit more advanced)

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Title</title>
    <link rel="modulepreload" href="./app.js" />
  </head>

  <body>
    <div id="app"></div>
    <script type="module">
      const app = document.querySelector('#app')
      app.innerHTML = 'loading...'
      try {
        const { renderApp } = await import("./app.js")
        app.innerHTML = 'loaded'
        renderApp(app)
      } catch(e) {
         app.innerHTML = 'An error occured'
      }
    </script>
  </body>
</html>

I convert HTML into the following:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Title</title>
    <script>
    /* Inlined version of s.min.js */
    </script>
    <link rel="preload" href="./app.js" as="script" />
  </head>

  <body>
    <script type="systemjs-module">
      System.register([], function (exports, module) {
        'use strict';

        return {
          execute: async function execute() {
            const app = document.querySelector('#app')
            app.innerHTML = 'loading...'
            try {
              const { renderApp } = await module.import("./app.js")
              app.innerHTML = 'loaded'
              renderApp(app)
            } catch(e) {
              app.innerHTML = 'An error occured'
            }
          }
        }
      })
    </script>
  </body>
</html>

(In reality async/await is even converted to a promise based version but it's not important and would hurt readability)

I think I have to use a module script because I want to import from a module. I don't think a regular script tag can do that in a clean way. One last thing: when browser support is good enough I am using the source files not the systemjs version.

Copy link
Collaborator

@joeldenning joeldenning left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the example above, you could have just used System.import('./app.js') rather than module.import('./app.js'), so I don't think that situation is one that requires this feature.

According to the code below, we're going to be deprecating systemjs-module, so I don't think it makes sense to extend the feature further:

// TODO: deprecate systemjs-module in next major now that we have auto import

The reason that we are planning on deprecating it is that auto-imports already automatically happen for systemjs modules that are in regular script tags. I've created https://codesandbox.io/s/intelligent-mahavira-jzkgk?file=/index.html which shows that inline systemjs modules already work in <script type="text/javascript">, so it's unclear to me why <script type="systemjs-module"> would be needed.

if (typeof inlineScriptContent === "string") {
if (inlineScriptContent.indexOf("//# sourceURL=") < 0)
inlineScriptContent += "\n//# sourceURL=" + url
;(0, eval)(inlineScriptContent)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I missed that, makes sense.

@dmail
Copy link
Contributor Author

dmail commented Sep 22, 2021

I was not aware of the auto import feature and how it works 🤔

The behaviour I would like to have from systemjs is the following:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Title</title>
  </head>

  <body>
    <!-- Not executed by SystemJS -->
    <script>
      console.log('Hello')
    </script>
    <script src="./jquery.js" />
    <!-- Executed by Systemjs -->
    <script type="systemjs-module">
      function helper() {
        console.log("Inline module executed!");
      }

      System.register([], () => ({
        execute() {
          helper()
        }
      }));
    </script>
    <script type="systemjs-module" src="./module.js"></script>
  </body>
</html>

Without "systemjs-module", how code could differentiate what should be executed and what shouldbe left to the browser? I am refering to <script src="./jquery.js" /> VS <script type="systemjs-module" src="./module.js"></script>

@dmail
Copy link
Contributor Author

dmail commented Sep 22, 2021

In the example above, you could have just used System.import('./app.js') rather than module.import('./app.js'), so I don't think that situation is one that requires this feature.

You mean I could write the "systemjs-module" as below ?

<script>
  (async function execute() {
    const app = document.querySelector('#app')
    app.innerHTML = 'loading...'
    try {
      const { renderApp } = await System.import("./app.js")
      app.innerHTML = 'loaded'
      renderApp(app)
    } catch(e) {
      app.innerHTML = 'An error occured'
    }
  })()
</script>

It would work indeed.
However I am generating the code inside the script tag not by hand but during a build process involving rollup. I am taking all scripts in the HTML (with an src or inline) and ask rollup to build them, afterwards I replace all <script type="module"></script> with <script type="systemjs-module"></script> and update their src or inline content.

@joeldenning
Copy link
Collaborator

Without "systemjs-module", how code could differentiate what should be executed and what shouldbe left to the browser?

SystemJS uses normal script tags to tell the browser to execute code, by creating <script type="text/javascript"> elements to load modules with System.import(). So there is no difference. Using eval() is not preferred over using <script>, since it requires loading the code as a string in JS memory whereas <script> does not require that. So SystemJS just uses <script> to execute modules.

You mean I could write the "systemjs-module" as below ?

Yes your code example works.

However I am generating the code inside the script tag not by hand but during a build process involving rollup. I am taking all scripts in the HTML (with an src or inline) and ask rollup to build them, afterwards I replace all <script type="module"></script> with <script type="systemjs-module"></script> and update their src or inline content.

I suggest converting the <script type="module"> elements with just <script type="text/javascript"> or simply <script>. No need to use systemjs-module in this case.

@dmail
Copy link
Contributor Author

dmail commented Sep 23, 2021

Now I get it, systems JS let browser do the job instead of systems module.
I'll try that today and let you know if that works thank you a ton

dmail pushed a commit to jsenv/core that referenced this pull request Sep 23, 2021
@dmail
Copy link
Contributor Author

dmail commented Sep 23, 2021

According to my tests it works when systemjs is loaded with a script tag (<script src="https://cdn.jsdelivr.net/npm/systemjs@6.10.3/dist/system.js"></script>)

However systemjs code won't auto import an inline script when it's inline in the html file.
This is because if (lastScript) { returns false

var scripts = document.querySelectorAll('script[src]');
var lastScript = scripts[scripts.length - 1];
if (lastScript) {
lastAutoImportUrl = lastScript.src;

@dmail
Copy link
Contributor Author

dmail commented Sep 23, 2021

I could make it work with the following changes (but I'm not sure these changes are correct, especially because I don't understand the need for setTimeout).

+  var inlineScriptCount = 0
   systemJSPrototype.register = function (deps, declare) {
     if (hasDocument && document.readyState === 'loading' && typeof deps !== 'string') {
       var scripts = document.querySelectorAll('script[src]');
       var lastScript = scripts[scripts.length - 1];
+      lastAutoImportDeps = deps;
       if (lastScript) {
         lastAutoImportUrl = lastScript.src;
-        lastAutoImportDeps = deps;
-        // if this is already a System load, then the instantiate has already begun
-        // so this re-import has no consequence
-        var loader = this;
-        lastAutoImportTimeout = setTimeout(function () {
-          autoImportCandidates[lastScript.src] = [deps, declare];
-          loader.import(lastScript.src);
-        });
       }
+      else {
+        inlineScriptCount++
+        lastAutoImportUrl = document.location.href + "__inline_script__" +inlineScriptCount ;
+      }
+      // if this is already a System load, then the instantiate has already begun
+      // so this re-import has no consequence
+      var loader = this;
+      lastAutoImportTimeout = setTimeout(function () {
+        autoImportCandidates[lastAutoImportUrl] = [deps, declare];
+        loader.import(lastAutoImportUrl);
+      });
     }

@joeldenning
Copy link
Collaborator

Regarding inlining the systemjs library itself, please create a separate github issue, as that seems unrelated to this pull request. I'm closing this pull request since the functionality already works using <script type="text/javascript">

@dmail
Copy link
Contributor Author

dmail commented Sep 28, 2021

Will do 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants