Skip to content

Dynamic import hook - #1790

Closed
guybedford wants to merge 9 commits into
rollup:masterfrom
guybedford:dynamic-import-hook
Closed

Dynamic import hook#1790
guybedford wants to merge 9 commits into
rollup:masterfrom
guybedford:dynamic-import-hook

Conversation

@guybedford

Copy link
Copy Markdown
Contributor

This implements a dynamic import hook for Rollup, roughly along the lines as discussed in #1779, although with a slightly different API.

The concept is that it should be possible to construct plugins for Rollup that can handle use cases such as:

  1. Module resolution of dynamic imports. Say import('lodash') being resolved to import('https://cdn.com/lodash')
  2. The ability to inline dynamic imports when the dynamically imported module is already in the Rollup bundle. So that import('y').then(y => console.log(y)) where y is already statically included becomes Promise.resolve(yNamespace).then(y => console.log(y))
  3. Module resolution of dynamic imports where there are expression cases. For example import('./local' + viewName) being updated to import('https://site.com/templates/' + viewName).

The API suggested to handle all of these cases is a plugin hook that looks like:

resolveDynamicImport (specifier: string | Node, parentId: string): Promise<string | void> | string | void

The cases then work out as the following:

  1. When there is a string used in the import expression (import('x')), we pass the string value directly as the value of specifier, and the parent module name as parentId. The returned string is then used as the new specifier to write into the dynamic import.
  2. If the string returned by the resolveDynamicImport function corresponds to a module name that is already in the bundle we are busy building, then we inline Promise.resolve(namespace) in its place.
  3. If the import expression cannot be found to be a simple string literal, then we call the resolveDynamicImport function with the specifier value as the original AST node. The returned value is then taken not to be a resolved module specifier literal, but rather the replacement source to use within the import expression.

Alternatively just look at the tests and I'm sure it will make sense.

With this, I can get dynamic chunking workflows around Rollup. Very excited by the possibilities I must admit.

Feedback very welcome - happy to climb hills to get this in.

@guybedford

Copy link
Copy Markdown
Contributor Author

It is worth also mentioning that this work is effectively flagged, as it requires the acorn: { plugins: { dynamicImport: true } } option to apply at all.

@lukastaegert

Copy link
Copy Markdown
Member

Hi @guybedford. Just note I am a little short on time working on rollup at the moment so reviewing this will take a few days, nevertheless thanks for all the effort you are putting in at the moment, I'll be in touch shortly.

@guybedford

Copy link
Copy Markdown
Contributor Author

@lukastaegert thanks for the update! No worries, when you are ready, and thanks for taking the time.

@anilanar

anilanar commented Dec 13, 2017

Copy link
Copy Markdown
Contributor

How does inlining behave exactly? Assume the following:

// a.js
console.log('destroy the world');

// b.js
document.addEventListener('click', () => {
  import('./a');
});

If inlining hoists importing of internal module ./a, then destroy the world will be logged unconditionally although that's not what we want.

Or does inlining work if and only if ./a was imported and included in the bundle by synchronous import statements? If that's the case, I'm assuming we'll need to have a separate bundling step for all files whose entry file is ./a. Assume the following:

// a.js
import { hugeFunction } from './c';
console.log(hugeFunction('destroy the world'));

// b.js
import { hugeFunction } from './c';
hugeFunction('foo');
document.addEventListener('click', () => {
  import('./a');
});

// c.js
console.log('a side effect');
export function hugeFunction(x) { /* a huge impl */ }

$ rollup src/a.js && rollup src/b.js

And in this scenario, hugeFunction and side-effects inside ./c are duplicated between bundles.

I'm sketching out some drawbacks/misconceptions rollup users may have using async imports so a relevant documentation can be updated accordingly.

@guybedford

Copy link
Copy Markdown
Contributor Author

@anilanar you've got the interpretations exactly right here in the second example - inlining only happens when the module is already included statically in the build. As for the use cases around dynamic import, some different thinking is needed to make these work out. I hope to share my project for handling deduplication along these lines next week!

@guybedford

Copy link
Copy Markdown
Contributor Author

Note one other optimization that could be added would be to use static analysis to trace which exports are used by the dynamic imported form.

For example:

import {b} from 'x';
import('x').then(x => {
  x.a();
});

Could know that we only use a and b from "x" and not all the other exports. That is, the inlining case should be able to apply tree shaking. And in the non-inlining case we should at least be able to return some metadata with the bundle informing which exports are used of the dynamic imports when we can deduce this (and being clear when we can't).

Happy to move in these sort of directions in due course with further PRs as well.

@lukastaegert lukastaegert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, so I had a long thought. These are my results:

  • Usually when people request an unsupported feature, we point them to using a transpiler. Obviously, this is not possible for dynamic imports.
  • Once dynamic imports become an official feature, it would be nice to already have solid and tested support.

To that end you got me convinced that we should have this feature. I like how you implemented the hook though I would like to extract the plugin code.

As for the implicit feature flag:
I would rather have a real rollup feature flag instead (that also takes care of any necessary acorn options).

  • This would abstract the plugin away from the user completely and allow us greater refactoring freedom
  • If the feature flag is not set, no plugin should be added to acorn; if it is set, no other options should be necessary
  • I would suggest to call it experimentalDynamicImports. Why "experimental"? Because I want it to be clear to the user that breaking changes to the feature are expected until we are sure this is "stable". That should give us (especially you!) more freedom in exploring different avenues (like automatic code splitting...) without (hopefully) angering the users too much.
  • Once we think this is stable AND acorn has official support, I would suggest to remove this feature flag again such that dynamic imports always work the way we decided they should work.
  • As plugins have the ability to modify options, dedicated plugins can change the feature flag for you if they need to.

Tell me what you think. I would really love to have you on board for this as you seem to have a lot of great ideas and the experience to support them.

Otherwise with the changes outlined above, I would merge this and create a new release.

Comment thread src/Module.js
};
});
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we could extract the plugin code to a separate file, that would be great. Especially since I expect this code to be removed as soon as acorn rolls out official support. This would also align nicely with one of my current goals to clean up Bundle.js and Module.js as much as possible 😉

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure I've done this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Btw, I made a PR to check how alive that plugin is: kesne/acorn-dynamic-import#11. In case it's not maintained I would also be willing to fork it.

description: 'Dynamic import expression replacement',
options: {
acorn: {
plugins: { dynamicImport: true }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I tried setting these flags to false in all tests but that did not have any effect. Are they actually required?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They are definitely required and will cause an error in acorn otherwise. I've definitely seen the error in my tests. let me know if you are still getting this though.

@guybedford

Copy link
Copy Markdown
Contributor Author

Thanks @lukastaegert for the feedback here, I'm really glad to hear that API sounds sensible to you.

An experimental flag would be good for this - I've added this as options.experimentalDynamicImport.

@lukastaegert lukastaegert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks a lot, from my side this is good to go into the next release!

Comment thread src/Module.js
@@ -1,4 +1,5 @@
import { parse, plugins as acornPlugins, tokTypes as tt } from 'acorn';
import * as acorn from 'acorn';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice touch, I think using the namespace here certainly improves readability

return function setModuleDynamicImportsReturnBinding ( _moduleDynamicImportsReturnBinding ) {
moduleDynamicImportsReturnBinding = _moduleDynamicImportsReturnBinding;
};
} No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd still prefer this to use the existing plugin, but I can understand that you don't want to have a dependency on another different acorn version. I hope kesne/acorn-dynamic-import#11 gets merged soon, otherwise I'll fork the repo.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree that having a publicly supported plugin is probably preferable as long as it is properly supported. However since now everything is behind a feature flag and the plugin logic is not exposed to the user, changing this later should not be a problem.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sure, this is fine as a start. Since I'm using acorn-dynamic-import in acorn-stage3, I want to make sure one way or the other that there is a maintained version.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Let me know if you end up forking it and I can change my pull request at kesne/acorn-dynamic-import#10 to point to the new fork.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

kesne/acorn-dynamic-import#11 is merged and there should be a release soon (after kesne/acorn-dynamic-import#10 landed, too). Should I prepare a PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@adrianheine if you're interested that would be amazing. Would likely have to come after #1857.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

See #1891.

@lukastaegert
lukastaegert changed the base branch from master to release-0.53 December 18, 2017 18:35
@guybedford

Copy link
Copy Markdown
Contributor Author

I was just going to say actually I might have an alternative approach here....

@guybedford

Copy link
Copy Markdown
Contributor Author

@lukastaegert can you DM me on twitter?

@usergenic

Copy link
Copy Markdown

+1 👏

@lukastaegert

Copy link
Copy Markdown
Member

Actually I kinda messed up this merge anyway because I overlooked some issues with another PR and think I might start assembling the release from scratch tomorrow. So no harm done, I can leave out this PR if you have got a better idea.

@lukastaegert

Copy link
Copy Markdown
Member

It’s kinda late today but we can talk tomorrow

@guybedford

Copy link
Copy Markdown
Contributor Author

Sure, happy to merge this as-is. I realise now what I was thinking is along the lines of a default dynamicImport hook, which fits into how all the hooks work, so there are no conflicts of direction.

But it would be good to chat about some of this some more at some point if you have a moment, as I've found it's easier to build this sort of tooling when being able to hook into Rollup through the Bundle API over the rollup.rollup API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants