embroider-build / ember-auto-import

Zero config import from npm packages
Other
360 stars 108 forks source link

Support changing the asset rootURL at runtime #409

Open ctcpip opened 3 years ago

ctcpip commented 3 years ago

I can't compile after upgrading to v2. Build is failing with:

Build Error (Inserter)

ember-auto-import could not find a place to insert app scripts in index.html.
ctcpip commented 3 years ago

in my index.html I do not have hardcoded script/src tags. Due to unique requirements of our application, the src path is modified dynamically at runtime. So I think it is failing in this bit of code that is parsing the file and looking for a <script> with a src attribute. Which it will never find.

ef4 commented 3 years ago

What do your <script> tags look like in index.html?

Can you share any customizations of outputPaths or fingerprint options in ember-cli-build.js?

Do you have a customized rootURL in config/environment.js?

Please run a build with the environment variable DEBUG=ember-auto-import:* and share the output.

ef4 commented 3 years ago

I replied above before seeing your second comment, yes, that must be the issue. Can you say more about why you need to do it that way? Maybe there's an easier alternative that would work with ember-auto-import, or maybe we need to make a more manual way for you to indicate where you want the auto-imported content to go.

ctcpip commented 3 years ago

"<script src=\"" + foo.bar + "{{rootURL}}assets/vendor.js\" charset=\"utf-8\"><\/script>"

ctcpip commented 3 years ago

without getting into too many gory details, the client needs to talk to the server to determine the root path due to requirements of an appliance in a datacenter with a reverse proxy...

ef4 commented 3 years ago

The challenge here is that we can produce some arbitrary number of bundles, depending on how many places your app and addons might say await import(). All of those bundles need this URL adjustment.

Some of those bundles will be entrypoint bundles and end up in script tags in index.html, and that is where this failure is coming from. We used to append the entrypoint bundles to vendor.js, but in 2.0 they became standalone files, which is better for caching and makes the build simpler.

And other bundles are lazy and will load only on demand, so you would also need to make sure webpack's runtime loader can find those. The way to do that is to set the __webpack_public_path__ global variable. Even on ember-auto-import 1.x, you need to be careful because if anybody (including your addons) uses await import() you'll get a lazy bundle that won't load correctly in production if you don't set that.

I think the most robust way to solve your problem is to let the build run in the normal way, and then postprocess the HTML. That way, you will be able to detect all assets that are located under rootURL and rewrite them to be under your dynamically discovered rootURL. For example, the build could produce:

<script src="/assets/vendor.js"></script>
<script src="/assets/chunk-14532812.js"></script>
<link rel="stylesheet" href="/assets/app.css"></script>

And your postprocessor would look at all <script> (and <link>, etc) tags and for any that have root-relative URLs, rewrite to something customized so that they can be handled by little bit of custom runtime loader code:

<my-custom-script src="/assets/vendor.js"></script>
<my-custom-script src="/assets/chunk-14532812.js"></script>
<script>
  (async function() {
    let rootURL = await discoverRootURL();
    __webpack_public_path__ = rootURL;
    document.querySelectorAll('my-custom-script').forEach(scriptTag => {
      let newTag = document.createElement('script');
      newTag.src = rootURL + scriptTag.src;
      document.body.appendChild(newTag);
    })
  })()
</script>

The benefit of this strategy is that you can avoid making any assumptions about what the exact assets will be. Your postprocessor reads the HTML and finds all of them no matter how they were produced.

ef4 commented 3 years ago

Oh, one thing I just remembered is that ember-auto-import already has some code that sets webpack_public_path, so we will likely need to make a change to avoid a collision when the app itself wants to take over that value.

ctcpip commented 3 years ago

allllright, here's what I ended up doing:

first, I created an in-repo-addon for the postprocessing (ember g in-repo-addon addon-name):

lib/addon-name/index.js

'use strict';

module.exports = {
  name: require('./package').name, // eslint-disable-line global-require

  async postBuild(results) {
    const fs = this.project.require('fs-extra');
    const jsdom = this.project.require('jsdom');

    const file = `${results.directory}/index.html`;
    const { JSDOM } = jsdom;
    const dom = await JSDOM.fromFile(file);
    const { document } = dom.window;

    document.querySelectorAll('script[src^="assets"],link').forEach(e => {

      const isLink = e.nodeName === 'LINK';

      const node = document.createElement(isLink ? 'clever-link' : 'clever-script');

      for (const a of e.attributes) {
        node.setAttribute(a.name, a.value);
      }

      if (isLink) {
        document.head.appendChild(node);
      }
      else {
        document.body.appendChild(node);
      }

      e.remove();

    });

    fs.writeFileSync(file, dom.serialize());

  },

  isDevelopingAddon() {
    return false;
  }
};

app/index.html

    <script>
    function queueResource(nodes, i) {

      const e = nodes[i];

      const isLink = e.nodeName === 'CLEVER-LINK';

      const node = document.createElement(isLink ? 'link' : 'script');

      for (const a of e.attributes) {
        node.setAttribute(a.name, ['href', 'src'].includes(a.name) ? myDynamicRootURL + a.value : a.value);
      }

      if (nodes.length > i + 1) {
        if (isLink) {
          queueResource(nodes, i + 1);
        }
        else {
          node.onload = function() {
            queueResource(nodes, i + 1);
          };
        }
      }

      if (isLink) {
        document.head.appendChild(node);
      }
      else {
        document.body.appendChild(node);
      }

      e.remove();

    }

    window.addEventListener('DOMContentLoaded', () => {
      __webpack_public_path__ = myDynamicRootURL; // eslint-disable-line camelcase, no-undef
      const nodes = document.querySelectorAll('clever-link,clever-script');
      queueResource(nodes, 0);
    });
    </script>

the tricky bit with this was the recursion. otherwise, loading scripts dynamically like this, they are not done being loaded/evaluated before the next script is loaded. so you get errors like define is undefined. hence the need to call queueResource within the onload event handler for scripts.

caveats

  1. need to get resolution regarding your comment about webpack_public_path
  2. I have not yet run this through our battery of regression tests (end-to-end testing), but all the ember tests pass
  3. ~I am still on ember-auto-import v1 - still need to upgrade to v2 again and see how it goes~ I upgraded to v2 and it's working, with all ember tests passing

thanks for your help @ef4 -- and code review of the above is more than welcome 😁