fastpack/fastpack

Dynamic imports

gilbert opened this issue · 10 comments

I have a lazy-loaded module similar to the following:

function loadIndexPage () {
  import("/assets/index-page.js").then(...)
}

However, fastpack is attempting to resolve and bundle /assets/index-page.js and can't find it since it's not a file but a url to my server. I would like fastpack to ignore dynamic imports and leave them in verbatim so the browser can do its thing.

Is this possible today or is it a feature that needs to be written? If the latter, I can try making a PR if you point me in the right direction on what to do.

Thank you for the interesting use-case. This isn't possible today without the code modification, but I see the couple of options how to approach it:

  1. (hacky, but quick) Currently, fastpack is greedy about all the import triggers in the code. For instance, it is going to fail on the following code, saying "import(_) is supported only with the constant argument":
    import(someVar).then(...)
    Not an ideal behaviour. This could be fixed by removing this match branch. Probably, this should be done regardless of other options anyway. But specifically to your case, you can modify your code as following:
    function loadIndexPage () {
      import("" + "/assets/index-page.js").then(...)
    }
    After these 2 steps, it should work.
  2. (more consistent). In order to avoid hacky code modification described above we may need to add another command-line option to skip some dynamic imports. Again, there are various choices how to do it (for example, use regexp to test the import request etc). However, maybe the simplest one would be --ignore-absolute-dynamic-import (the name may be better though). If specified it would ignore all the import() requests starting with '/'.

Thoughts?

Thanks for the reply. Thinking about it further, it seems best for fastpack to not touch them at all. Dynamic imports exist to avoid creating static dependencies. If fastpack attempts to bundle a dynamic import, then it is no longer dynamic, and there is no longer a reason for import() to return a promise.

Doesn't require() already take care of this? Since browsers don't have a require function, it makes sense for fastpack to bundle non-toplevel require statements. This cover the "lazy module loading" use case that I think this import() behavior is trying to do, and is also synchronous.

I just thought of a manipulation use case for fastpack: dynamically importing libraries. For example, telling fastpack to rewrite require('moment').then(...) to a configured url format like require('/assets/node_modules/moment'). Browsers currently do not handle bare specifier like 'moment', so something will be needed in this case. But to be clear this is not the use case I need right now.

Thinking about it further, it seems best for fastpack to not touch them at all. Dynamic imports exist to avoid creating static dependencies. If fastpack attempts to bundle a dynamic import, then it is no longer dynamic, and there is no longer a reason for import() to return a promise.

You're totally right! Currently fastpack bundles all the import()s statically into one bundle. But, while "cheating" on the implementation, it stays fair with an interface returning the promise. The reason for it is just the fact that chunked bundles are not currently supported, (but exist in a plan). Once we add those, each of the import('module') expressions will form another *.chunk.js file in the output directory and will be loaded at runtime only if needed. So, I doubt, fastpack could entirely ignore them. But as I mentioned earlier fastpack should definitely ignore all the import(...) expressions, which argument is not a string literal.

Maybe, we could come up with some rules on how to ignore it even with the string literal argument, guessing what user had on mind? For example:

  • starts with '/': user really wants the absolute URL, not file. Very unlikely you'll keep the absolute file path in the code
  • starts with 'http:'/'https:': definitely an absolute URL

It makes sense to me, and seems to work for your use case?

As for the require('moment').then(...), I'm not sure we could change the semantics and interface of the require. It looks inconsistent, given that the same function should return different result type depending on the placement in the source code.

The reason for it is just the fact that chunked bundles are not currently supported, (but exist in a plan)

I do appreciate that fpack plans to be intelligent about dynamic imports. What I'm concerned about is overriding defaults. Many browsers (and soon, node.js) support the import() syntax, and I wouldn't want fpack to implicitly change default behavior:

  • import('./my-mod') and import('/some/path/x.js') requires a file in node, and fetches a url in the browser
  • import('moment') requires a module in node, and is unspecified in the browser
  • import('https://...') is (probably?) unspecified in node, and fetches a url in the browser.

(node.js behavior is important to consider for those who want to run their code on the server, e.g. server-side rendering)

Overriding these behaviors should be opt-in IMO.

Once we add those, each of the import('module') expressions will form another *.chunk.js file in the output directory and will be loaded at runtime only if needed.

👍

Maybe, we could come up with some rules on how to ignore it even with the string literal argument

These rules look good to me. But to be clear I prefer fpack not to bundle dynamic imports by default. I'm trying to split my code for lazy loading, so I hope for a way to turn off the current fpack behavior until chucked bundles are supported.

As for the require('moment').then(...), I'm not sure we could change the semantics and interface of the require

Oops, that comment was meant for import, not require. This is a second feature on my wishlist: having fpack rewrite import('moment') to import('/my/url/moment') so I can have library sharing across my manually-chunked bundles. But that can come later.

MoOx commented

Once we add those, each of the import('module') expressions will form another *.chunk.js file in the output directory and will be loaded at runtime only if needed. So, I doubt, fastpack could entirely ignore them.

I understand code splitting will be a feature, am I correct? If so I am glad to read that :)
This days, it's definitely something needed in production, especially for a tool that talks about speed (even if, compilation speed clearly differs from serving speed).

@MoOx it is almost a feature already :) Working in a branch at the moment. Plan to release it this or next week:
https://github.com/fastpack/fastpack/tree/feat/code-splitting

MoOx commented

😱♥️

natew commented

Saw this branch is missing now, but not an update, any word on code splitting?

Hi @natew! Thanks for your interest. The code splitting is released, here are release notes:

https://github.com/fastpack/fastpack/releases/tag/v0.7.0

From the user standpoint it works in the same way as in webpack/parcel. I.e. using import("some-module") would potentially (unless "some-module" was not previously statically imported) create another JS file in the output directory and then automatically load it at runtime.

Depending on your server configuration you may need to specify the --public-path option at build time and/or set it in the code using __public__path__ or __webpack_public_path__.