Giter VIP home page Giter VIP logo

proposal-dynamic-import's Introduction

import()

This repository contains a proposal for adding a "function-like" import() module loading syntactic form to JavaScript. It is currently in stage 4 of the TC39 process. Previously it was discussed with the module-loading community in whatwg/loader#149.

You can view the in-progress spec draft and take part in the discussions on the issue tracker.

Motivation and use cases

The existing syntactic forms for importing modules are static declarations. They accept a string literal as the module specifier, and introduce bindings into the local scope via a pre-runtime "linking" process. This is a great design for the 90% case, and supports important use cases such as static analysis, bundling tools, and tree shaking.

However, it's also desirable to be able to dynamically load parts of a JavaScript application at runtime. This could be because of factors only known at runtime (such as the user's language), for performance reasons (not loading code until it is likely to be used), or for robustness reasons (surviving failure to load a non-critical module). Such dynamic code-loading has a long history, especially on the web, but also in Node.js (to delay startup costs). The existing import syntax does not support such use cases.

Truly dynamic code loading also enables advanced scenarios, such as racing multiple modules against each other and choosing the first to successfully load.

Proposed solution

This proposal adds an import(specifier) syntactic form, which acts in many ways like a function (but see below). It returns a promise for the module namespace object of the requested module, which is created after fetching, instantiating, and evaluating all of the module's dependencies, as well as the module itself.

Here specifier will be interpreted the same way as in an import declaration (i.e., the same strings will work in both places). However, while specifier is a string it is not necessarily a string literal; thus code like import(`./language-packs/${navigator.language}.js`) will work—something impossible to accomplish with the usual import declarations.

import() is proposed to work in both scripts and modules. This gives script code an easy asynchronous entry point into the module world, allowing it to start running module code.

Like the existing JavaScript module specification, the exact mechanism for retrieving the module is left up to the host environment (e.g., web browsers or Node.js). This is done by introducing a new host-environment-implemented abstract operation, HostPrepareImportedModule, in addition to reusing and slightly tweaking the existing HostResolveImportedModule.

(This two-tier structure of host operations is in place to preserve the semantics where HostResolveImportedModule always returns synchronously, using its argument's [[RequestedModules]] field. In this way, HostPrepareImportedModule can be seen as a mechanism for dynamically populating the [[RequestedModules]] field. This is similar to how some host environments already fetch and evaluate the module tree in ahead of time, to ensure all HostResolveImportedModule calls during module evaluation are able to find the requested module.)

Example

Here you can see how import() enables lazy-loading modules upon navigation in a very simple single-page application:

<!DOCTYPE html>
<nav>
  <a href="books.html" data-entry-module="books">Books</a>
  <a href="movies.html" data-entry-module="movies">Movies</a>
  <a href="video-games.html" data-entry-module="video-games">Video Games</a>
</nav>

<main>Content will load here!</main>

<script>
  const main = document.querySelector("main");
  for (const link of document.querySelectorAll("nav > a")) {
    link.addEventListener("click", e => {
      e.preventDefault();

      import(`./section-modules/${link.dataset.entryModule}.js`)
        .then(module => {
          module.loadPageInto(main);
        })
        .catch(err => {
          main.textContent = err.message;
        });
    });
  }
</script>

Note the differences here compared to the usual import declaration:

  • import() can be used from scripts, not just from modules.
  • If import() is used in a module, it can occur anywhere at any level, and is not hoisted.
  • import() accepts arbitrary strings (with runtime-determined template strings shown here), not just static string literals.
  • The presence of import() in the module does not establish a dependency which must be fetched and evaluated before the containing module is evaluated.
  • import() does not establish a dependency which can be statically analyzed. (However, implementations may still be able to perform speculative fetching in simpler cases like import("./foo.js").)

Alternative solutions explored

There are a number of other ways of potentially accomplishing the above use cases. Here we explain why we believe import() is the best possibility.

Using host-specific mechanisms

It's possible to dynamically load modules in certain host environments, such as web browsers, by abusing host-specific mechanisms for doing so. Using HTML's <script type="module">, the following code would give similar functionality to import():

function importModule(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
    script.type = "module";
    script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;

    script.onload = () => {
      resolve(window[tempGlobal]);
      delete window[tempGlobal];
      script.remove();
    };

    script.onerror = () => {
      reject(new Error("Failed to load module script with URL " + url));
      delete window[tempGlobal];
      script.remove();
    };

    document.documentElement.appendChild(script);
  });
}

However, this has a number of deficiencies, apart from the obvious ugliness of creating a temporary global variable and inserting a <script> element into the document tree only to remove it later.

The most obvious is that it takes a URL, not a module specifier; furthermore, that URL is relative to the document's URL, and not to the script executing. This introduces a needless impedance mismatch for developers, as they need to switch contexts when using the different ways of importing modules, and it makes relative URLs a potential bug-farm.

Another clear problem is that this is host-specific. Node.js code cannot use the above function, and would have to invent its own, which probably would have different semantics (based, likely, on filenames instead of URLs). This leads to non-portable code.

Finally, it isn't standardized, meaning people will need to pull in or write their own version each time they want to add dynamic code loading to their app. This could be fixed by adding it as a standard method in HTML (window.importModule), but if we're going to standardize something, let's instead standardize import(), which is nicer for the above reasons.

An actual function

Drafts of the Loader ideas collection have at various times had actual functions (not just function-like syntactic forms) named System.import() or System.loader.import() or similar, which accomplish the same use cases.

The biggest problem here, as previously noted by the spec's editors, is how to interpret the specifier argument to these functions. Since these are just functions, which are the same across the entire Realm and do not vary per script or module, the function must interpret its argument the same no matter from where it is called. (Unless something truly weird like stack inspection is implemented.) So likely this runs into similar problems as the document base URL issue for the importModule function above, where relative module specifiers become a bug farm and mismatch any nearby import declarations.

A new binding form

At the July 2016 TC39 meeting, in a discussion of a proposal for nested import declarations, the original proposal was rejected, but an alternative of await import was proposed as a potential path forward. This would be a new binding form (i.e. a new way of introducing names into the given scope), which would work only inside async functions.

await import has not been fully developed, so it is hard to tell to what extent its goals and capabilities overlap with this proposal. However, my impression is that it would be complementary to this proposal; it's a sort of halfway between the static top-level import syntax, and the full dynamism enabled by import().

For example, it was explicitly stated at TC39 that the promise created by await import is never reified. This creates a simpler programming experience, but the reified promises returned by import() allow powerful techniques such as using promise combinators to race different modules or load modules in parallel. This explicit promise creation allows import() to be used in non-async-function contexts, whereas (like normal await expressions) await import would be restricted. It's also unclear whether await import would allow arbitrary strings as module specifiers, or would stick with the existing top-level import grammar which only allows string literals.

My understanding is that await import is for more of a static case, allowing it to be integrated with bundling and tree-shaking tools while still allowing some lazy fetching and evaluation. import() can then be used as the lowest-level, most-powerful building block.

Relation to existing work

So far module work has taken place in three spaces:

This proposal would be a small expansion of the existing JavaScript and HTML capabilities, using the same framework of specifying syntactic forms in the JavaScript specification, which delegate to the host environment for their heavy lifting. HTML's infrastructure for fetching and resolving modules would be leveraged to define its side of the story. Similarly, Node.js would supply its own definitions for HostPrepareImportedModule and HostResolveImportedModule to make this proposal work there.

The ideas in the Loader specification would largely stay the same, although probably this would either supplant the current System.loader.import() proposal or make System.loader.import() a lower-level version that is used in specialized circumstances. The Loader specification would continue to work on prototyping more general ideas for pluggable loading pipelines and reflective modules, which over time could be used to generalize HTML and Node's host-specific pipelines.

Concretely, this repository is intended as a TC39 proposal to advance through the stages process, specifying the import() syntax and the relevant host environment hooks. It also contains an outline of proposed changes to the HTML Standard that would integrate with this proposal.

proposal-dynamic-import's People

Contributors

ctrimm avatar domenic avatar domfarolino avatar jakearchibald avatar littledan avatar probins avatar robpalme avatar rwaldron avatar styfle avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

proposal-dynamic-import's Issues

Should module completion value be fulfillment value of the promise?

When the initialization of a module completes normally it has a completion value that is generally the value of the last expression evaluated within the module body (see 15.2.1.20). For static imports this completion value is discarded because there is no way for ES code to capture and process it.

However, dynamic-import is a different situation in that it returns a Promise to the ES code. The fulfillment value of that promise could be used to transmit the module completion value back to the ES code that dynamically imported the module.

For this to work with multiple imports (either static or dynamic) of the same module, the completion value would have to be captured in the module record by the import that actually evaluates the body.

A Use Case

One reason that might be desirable is to enable a dynamically imported module to return a require-like export object:

//foo.js
//static exports accessible via static import
export function bar() {}
export function baz() {}
//make foo and bar available to dynamic importers
({bar, baz});   //this is the module completion value

//client.js
import("./foo.js").then(foo => {
   foo.bar();
   foo.baz();
});

A Concern

(attention @erights)

It seems like such a dynamic module completion value could be used to create a communications channel that could pierce an intra-realm membrane. Consider a collaborator, outside of the membrane, does a dynamic import of module "X" and obtains an object as the module's completion value. The collaborator adds a property to the object and uses it to store private data that only exists outside the membrane. Code within the membrane also does a dynamic import of module "X" and obtains the same completion value object. That code now has access to the private data stored in the object by the collaborator.

Paths mapping support

Proposal: add support for mapping between module name and path (URL)

When the URL of the module is not known at runtime, but the module name is known (web applications with plugins) the map configuration of some module loaders is very handy (e.g. https://github.com/systemjs/systemjs/blob/master/docs/getting-started.md#map-config, http://requirejs.org/docs/api.html#config-paths). The ability to resolve specific module name to different URLs is important for large and pluggable web applications.

Assumption

As far as I understand, after dynamic import is available in browsers I could write something like this:

let mymodule = await import("http://no-cors-issues.url-known-at-runtime-only.org/mymodule");

Motivation

Easy handling of common code base (server and front-end) for pluggable applications:

In case same module needs to be loaded from one place in browser (lets say CDN) and from other place in server environment (let's say node_modules, resolved from module name), the naive approach would look like:

common-code.js

let myModuleName = null
if (isNodeJs()) myModuleName="mymodule";
if (isBrowser()) myModuleName="http://no-cors-issues.url-known-at-runtime-only.org/mymodule";
let mymodule = await import(myModuleName);

Instead of:
browser-only.js

import.map({
  "mymodule":"http://no-cors-issues.url-known-at-runtime-only.org/mymodule"
})

common-code.js:

let mymodule = await import("mymodule");

Consider another example:
common-code.js

getDynamicModulePath(moduleName) {
  let mappings = {
    "mymodule":"http://no-cors-issues.url-known-at-runtime-only.org/mymodule"
  };
  return mappings[moduleName] ? mappings[moduleName] : moduleName;
}

let mymodule = await import(getDynamicModulePath("mymodule"));

This example represents portable, but very ugly code that could be implemented on top of dynamic import.

Rename repo to proposal-import-function-like?

The name of this proposal is import(). But people need an alphanumeric name for URLs and babel plugins and such. I think "import-function" might be misleading since it's not a function. Perhaps "import-function-like" would be better? Suggestions welcome.

import default operator

It may be beyond the current proposal, but I've just received some feedback. (BG: dynamic-import is now implemented in WebKit nightly).

In the static import syntax. we have 3 forms: import * as namespace, import { named }, and import deafult.

On the other hand, the current dynamic-import proposal always resolves the result promise with the namespace object.
By using this object and existing ES syntax, we can write the code that is similar to the second named form.

const { named } = await import("...");

However, for the third case, we need to write a bit storage code like this.

const { default:map } = await import("../map.js");

or

const map = (await import("../map.js")).default;

In the static import syntax, we have the import default syntax separately while we can write the named import syntax like the following.

import {default as map} from "../map.js"

This default export/import mechanism encouraged modules to export the one representative default export and helped the developer to just use the default exported one instead of specifying the exported binding names.

So here, I would like to discuss about introducing the similar thing to the dynamic import. Like,

import default ("../map.js")

Is it expected that module namespace objects are treated as thenables?

Module file "m.js":

export function then(resolve, reject) {
    resolve("Is it expected that module namespace objects are treated as thenables?");
}
import("./m.js").then(v => print(v));

When loaded with jsc -m ./m.js or d8 --harmony_dynamic_import --module ./m.js, it prints "Is it expected that module namespace objects are treated as thenables?". So, is it expected?

Question: Hot-reloading a dynamic module

Is there a way to hot reload a dynamic module? My current understanding is that if I were to run the import(x) a second time it would not reload the module. Would it make sense to add an additional flag on the call with value such as never(default), if updated, always/force.

Add some examples

The current readme doesn't really help people understand what this is actually about at a glance; they have to read the whole thing.

I tried this briefly but my examples became a bit too large. Probably if I try again on a fresh day it'll be fine. But contributions from others would certainly be appreciated too.

revised proposal still violates run-to-completion execution semantics

The revised HostImportModuleDynamically says

If the operation succeeds, then either immediately or at some future time the host environment must perform FinishDynamicImport(referencingScriptOrModule, specifier, promiseCapability, NormalCompletion(undefined)).

The phrase "some future time" does not appear to place any restriction on the timing of performing FinishDynamicImport. So, it could occur in the middle of any ES job without waiting for completion of the current job (or even assume atomic operations). FinishDynamicImport calls HostResolveImportedModule which may start evaluation of a module body. So, we are still potentially preemptively interrupting ES code execution and observably running other ES code.

You could avoid this by scheduling a job to do FinishDynamicImport. As I stated in #24 (comment) ES spec. jobs provide the spec. mechanisms to schedule the execution of asynchronous ES code without violating the run to completion invariants

Other issues

then either immediately or at some future time.

That means module initialization code might run now or might run latter. In other parts of ES it was decided we would always defer to at least the "next turn" in such situations in order to improve deterministic behavior. If it is a maybe now/maybe latter we choose to make it be always latter.

If the operation succeeds, a subsequent call to HostResolveImportedModule after FinishDynamicImport has been performed, given the arguments referencingScriptOrModule and specifier, must complete normally.

-"has been performed" or "has been completed"? If this is happening asynchronously and concurrently FinishDynamicImport may have started but not yet processed to determination of success or failure. Can't multiple FinishDynamicImport for the same thing be inflight at the same time?

  • The meaning of "If the operation succeeds" and "If the operation fails also seems fuzzy for similar reasons.

I think you are trying to address consistency and idempotency requirements but the more I try to understand the language the less certain I am as to what it actually means. Consider the following:

 import("perhaps this file doesn't exist").then(m1=>console.log("succeed 1",_=>console.log("fail 1"));
 import("perhaps this file doesn't exist").then(m2=>console.log("succeed 2",_=>console.log("fail 2"));

Ignoring asychnc ordering, can we ever get "succeed 1 fail2" or "fail 1 succeed 2". If the result is "succeed 1 succeed 2" do m1 and m2 every have different values?

How to express implicit dependencies?

With static imports:

import 'es5-shim';
import 'es6-shim';

theoretically downloads both resources in parallel, but executes them in order - which is good, because es6-shim depends on es5-shim being run first. Said another way, lexical order of static imports also expresses an implicit dependency on execution order.

How can I achieve this with import()?

One obvious solution is that I could import('es5-shim').then(() => import('es6-shim')) - but then the resources are not downloaded in parallel.

Another obvious solution is that I could create a file, "shims.js", that consisted of the static imports mentioned previously - and then import('./shims.js') would issue one request to download that file, followed by two parallel requests that were then executed in order. This isn't bad, but requires a) an additional HTTP request, and b) an additional file.

webpack/webpack#3141 is another example of this same problem using Intl polyfills.

Also obviously, implicit dependencies like this are subpar - ideally es6-shim should import es5-shim, thus making the dependency explicit; and ideally intl/locale-data/jsonp/en should import intl, thus making that dependency explicit. However, this clearly isn't always possible.

One possibility is wasting future extensibility by making import() take multiple arguments, such that arguments would all be downloaded in parallel but executed in order and a single promise for all returned; another is allowing import() to take an array as well as a string (which would be a runtime thing, or else it would have to be an array literal) and when an array is passed, the same behavior occurs. However, I'm not sure whether those are a good idea.

Do you think this is something import() can solve? If not, what would you suggest as an alternative? If so, what approach do you think would be the least objectionable?

Tree shaking and partial imports.

Is this something that can be done here?

I figured something like

import('lodash', 'map').then(map => ...)

Just curious if this has been thought of.

Is import() a function?

Thanks for writing this up. I really love this spec because it feels very naturally to me to use an import() function when you want to import things on runtime and an import statement when you want to import things statically.

Although I really like the static nature of ES2015 modules, I also see the use-case for dynamic imports which enabled us to provide fallbacks in node when a module was not usable for whatever reason.

However, after reading the proposal I had some questions: 😁

If I understood it correctly, import() would not be a regular function since you need information about the importing module (for instance when resolving relative specifiers). Is that correct? How will this extra-information be passed into the HostResolveImportedModule? Since import() feels like a function, will I be able to do function-like things like:

const myImport = import;

typeof import === "function";

And how does this relate to System.import()? Is System.import() a regular function which means that module-relative specifiers would be impossible to implement?

Expose import(id) to non-module scripts?

If I understand correctly, prior to this proposal, an HTML <script type=module> tag like

<script type=module>
  import "./main.js";
</script>

was more or less equivalent to a non-module <script> tag that called System.import:

<script>
  System.import("./main.js");
</script>

What are the chances of using import(id) in this way, so that we can fully deprecate the global System.import API?

<script>
  import("./main.js");
</script>

Since import is a reserved keyword in scripts, there should be no problem with giving it a new meaning, a la typeof or super.

Presumably the parent module identifier used to resolve the imported identifier—that is, what exactly is "./main.js" relative to?—would either be undefined or something like the path of the calling script or HTML file.

export a promise as module to help import() resolving the dep chain in child import()s

the module statement in 2.js is like the cmd module.exports = something or amd return something to export a top level value.

in this case when import() received a promise as module, it will keep waiting till the promise chain is resolved or any of them is rejected

1.js

import('./2.js').then(m => console.log(m)); //will be: 2.js with 3.js and 4.js

2.js

module Promise.all([import('./3.js'), import('./4.js')])
  .then(([m3, m4]) => '2.js with ' + m3.m3 + ' and ' + m4.m4));

3.js

export let m3 = '3.js';

4.js

export let m4 = '4.js';

What do we call this?

In conversation about it, we don't want to call it the "import function" because it's function-like and we don't want people to be confused. So what do we call it? If I were to say: "And here, we pass the path to the module to the import _______"

Dynamic import I guess? I'm up for ideas :) But we should probably decide before people start calling it the "import function"

DOM handler and import

I'm not confident that this is the right place to discuss about it.

We can write the code for the handler in HTML, like,

<body onload="import("./a.js");"></body>

In that case, should we allow import operator inside this handler?
If we allow it, I think we have no way to specify crossorigin, nonce etc. parameters for the subsequent fetching request for this import operator.

Asynchronous spec mechanics

When it asynchronously completes, let fetchResult be the resulting asynchronous completion value, and continue to the next step.

Can we do this? I thought the spec algorithm steps were synchronous.

Multiple imports

Forgive me if I missed something. But is there a provision for multiple imports / promises for this?

i.e. import(['module1', 'module2']).then(function(module1, module2) {

});

Been using something like this since jspm/systemjs and requirejs and I always see ugly Promise.all solutions which are some heinous syntax that I hope doesn't make it into the proposal. A little sugar would be great.

Named vs default imports?

Let's say I have a module that can be imported like so:

import foo, { bar } from 'path';

How can I use import() to get both foo and bar? In the following code:

import('path').then((x) => {
  // what is `x` here? `foo`? `bar`? an object?
});

Optional Module Name Enumeration

Given this proposal it's entirely possible to have code of the type:

function getUserInput() {
  return Math.random();
}
let moduleName = getUserInput();
import(moduleName);

This is fantastic for programming user ergonomics and literally impossible to do anything with tooling-wise for ahead-of-time optimization (which impacts the programming user's experience). The options I see here to make it possible to do AOT optimizations are:

  1. Discourage use of this feature. (Not ideal.)
  2. Leverage comments as a secondary information channel to any tooling. (Not ideal, but will work just as well as encoding it into the dynamic import itself for AOT tools.)
  3. Add additional (optional) annotation features into the import function. Can be used as a must-be-satisfied constraint for security benefits as well. (Also not entirely ideal because of increased API surface area, but maybe worth it?)

I'd like to propose modification to allow for option three above:

import(moduleName, ['allowed', 'also-allowed']);

I'm on @ember-cli/core; we care about the outcome of this discussion. 😄

Alternative approach: Import on demand

"Being able to know the dependencies after parsing" seems to be one big issue why dynamic imports are not allowed. This proposal goes straight against that. With this understanding I thought that the following approach might be a solution:

import on-demand helperFunction from './views/**/*.js'

helperFunction('./views/MyView.js')
  .then(function (view) {
     // Here the view can be used.
  })

With a syntax like:

import on-demand <variable-name> from <grep-path>

where variable-name will contain a reference to a helper method that allows to load all files that match <grep-path>, returning a Promise.

This way the parser would get a full variable path name that could be used for the dynamic lookup.
The loader would need to keep all the files of the given grep handy. Not quite sure how that would work in the browser environment (HEAD requests?) but it could be a compromise?!

How to get named exports ?

// files/b.js

export const myVar = 1;
export default function () {}

// files/a.js

import b from './b.js';
export default { b: b };

// index.js

import('files/a.js').then(a => {
	console.log(a.b.myVar); // NOT WORKING !!!
        console.log(a.default.b.myVar); // NOT WORKING !!!
        console.log(a.default.b.default.myVar); // NOT WORKING !!!
});

what am I doing wrong ?

Thanks!

Bad specifier always leads to rejected Promise?

This seems obvious from the spec, but please confirm:

Bad specifiers (e.g. non-strings, bad URLs, nonsense unicode) will never cause import() to throw.
Instead, import() will return a rejected promise.

import syntax with import declaration

When we have the following script in the module context,

// The goal symbol is Module.
import("./x.js");  // Top level!

When we see import keywrod, we encounter ambiguity between import declaration and import call.
Is this understanding correct?
I think we need [lookahead != (] for import declarations.

Stage 3 reviews!

Here are the people who have signed up as reviewers for this spec that need to sign off before it can get to stage 3, per the notes:

And of course the editor:

Also, we have one "unofficial reviewer" whose feedback would be welcome:

Useful resources, perhaps:

Always Async: Flash of Loading Content

On the web, the only use-case I can think of is code splitting. I've been doing code-splitting for a few years now manually, with AMD, and for the last couple years with webpack.

While intuitively a promise seems like the most obvious async primitive here I'd argue a callback would be better for product developers and website users, why? Flash of Loading Content.

The first time somebody visits a code-split page, the module is not yet loaded so the product developer will add some loading indicator.

The second time the user visits a code-split page, the browser already has that code cached, but promises are asynchronous always, so the user gets a split second flash of the loading screen. It feels awful. Dancing around this problem with the declarative UI patterns emerging is difficult.

I've been using the bundle loader for webpack for a couple years now. It is asynchronous the first time, synchronous every time after. It makes for a great user and developer experience.

I messed around w/ import() and code-splitting by trying to keep my own module cache with some sort of syntax like this

loadBundle('./thing', () => import('./thing'), (mod) => { })

Really redundant to have to specify the path in two places every time you want to code split. Also, building a module cache isn't a trivial thing for a lot of people.

I'd really like to see something that doesn't cause a Flash of Loading Content. I'd continue to use custom module loading with webpack and skip the platform. Which is a shame since it seems this use case is the primary motivator for import()!

Make importing fail if URL has changed (for SPA, PJAX, and History API)

After url and dom are changed by SPA or PJAX, requested scripts must NOT be evaluated. So importing must have failed if url has changed.

If it is not a module script, we can cancel the evaluation of that. But if it is a module script, we cannot cancel the evaluation of that.

Here is my code having this problem.

  const url = new URL(standardizeUrl(location.href));
  if (script.type.toLowerCase() === 'module') {
    return wait.then(() => import(script.src))
      .then(
        () => (
          void script.dispatchEvent(new Event('load')),
          Right(script)),
        reason => (
          void script.dispatchEvent(new Event('error')),
          Left(new FatalError(reason instanceof Error ? reason.message : reason + ''))));
  }
  else {
    return script.hasAttribute('defer')
      ? wait.then(evaluate)
      : evaluate();
  }


  function evaluate() {
    try {
      if (new URL(standardizeUrl(location.href)).path !== url.path) throw new FatalError('Expired.');
      if (skip.has(standardizeUrl(location.href))) throw new FatalError('Expired.');
      void (0, eval)(code);
      script.hasAttribute('src') && void script.dispatchEvent(new Event('load'));
      return Right(script);
    }
    catch (reason) {
      script.hasAttribute('src') && void script.dispatchEvent(new Event('error'));
      return Left(new FatalError(reason instanceof Error ? reason.message : reason + ''));
    }
  }

https://github.com/falsandtru/pjax-api/blob/ae05475a829974d634b46ac346939a599c90545b/src/layer/domain/router/module/update/script.ts#L123-L152

"Tradeoffs" or "risks" section to the proposal?

Do you think it would be useful to add a "tradeoffs" or "risks" section to the proposal? At this moment it may seem as if it has no downsides to consider. However, some points were raised (and dismissed) in the issues that indicate potential problems arising from the proposal, for example:

  • breaking existing bundlers #28
  • breaking tree-shaking at least for same cases #19
  • making dead code elimination analysis harder

It seems like a common practice for language enhancement proposals, see for example http://openjdk.java.net/jeps/286 (or any other JEP).

Possible Playground

Have a look at this little utility which aim is to bring CommonJS like environment and a Promise based module.import().

The path is always relative to the current module, but you can use external paths or absolute path to thhe root of the site/computer, if needed.

How to load modules (both node and browsers)

// single module
module.import('./cool-stuff').then(function (coolStuff) {
  coolStuff('!');
});

// multiple modules
Promise.all([
  module.import('./cool-stuff'),
  module.import('./even-better')
]).then(function (modules) {
  const [coolStuff, evenBetter] = modules;
});

How to export modules (both node and browsers)

// synchronous export
module.exports = (...args) => { console.log(args); };

// async export
module.exports = new Promise((resolve) => {
  // do anything async then ...
  setTimeout(
    resolve,
    1000,
    (...args) => { console.log(args); }
  );
});

// async after dependencies
module.exports = Promise.all([
  module.import('./cool-stuff'),
  module.import('./even-better')
]).then(function (modules) {
  const [coolStuff, evenBetter] = modules;
  return {method(inout) { return evenBetter(coolStuff(inout)); }};
});

If you have any question I'd be happy to answer (if I have answers).

Best Regards

Error Asymmetry with declaration form

Something that the declarative import form is able to do but the import() form is not able to do is throw errors on missing or ambiguous imports, e.g. these can throw errors with the declaration form:

import foo from "./foo.js"
// SyntaxError: The requested module does not provide an export named 'foo'
import { foo } from "./exportStars.js"
// SyntaxError: The requested module does not provide an export named 'foo'
// SyntaxError: The requested module contains conflicting star exports for name 'foo'

Now you definitely can create functions that do these behaviours e.g.:

async function importDefault(module) {
    const ns = await import(module)
    if (!Reflect.has(ns, 'default')) {
        // It might be useful to actually have the host error message  instead so that it can be known if ambiguous or missing
        throw new SyntaxError(`Some error message`)
    }
    return ns.default
}

async function importNames(names) {
    // Same thing as default but for many
}

However this suffers the same issue as #37 (comment) in that you can't share it around different modules so you wind up having to reimplement it everywhere.

I'm not sure what the solution would be, it might even be a part of the loader API instead but I could imagine something like on the loader issue of having import.names where you can specify names you want.

e.g.

async function validateStudentCode() {
    const { parseJS } = await import.names('/validators.js', ['parseJS'])
}

An alternative could be that the import() form doesn't return a normal Module object but rather another object that has someModule.someNonexistentName be a getter that throws the error that would've happened e.g. (await import('./foo.js')).nonExistentName would throw the same error as import { nonExistentName } from "./foo.js".

Error: No NgModule metadata found for 'undefined' for typescript converted js file

Hi,
I am able to load module dynamically using import() method for angular modules in developing mode where I used typescript. i.e. when modules are written in typescript its working fine. Then I tried it in production mode and build all my typescript and generate js bundle and load it into web server. In that case it is not working and I am getting the following error Error: No NgModule metadata found for 'undefined' to load the module.
The scenario where I am getting error is, I developed some plugins with component, module and service, then build the typescript files to get the JS files and import ...module.js file using import() method. In that case, it is not working. But whenever I am using typescript plugins with angular-cli then it works fine. How can I import external JS module by avoiding the mentioned error? Thanks.

Consider introducing DynamicModuleEvaluationJob

Conceptually, I find it easiest to think about "dynamic import" as an operation that schedules a new Module Evaluation Job that will settle a promise when it completes. I think that the spec. text can be made clearer and simpler if it follows this approach. For example, I believe that by using this approach, the vaguely defined async language can be removed from the ImportCall evaluation semantics and also from HostPrepareImportedModule and centralized in the abstract operation that defines the behavior of a dynamic import evaluation job. That should allow the direct use of HostResolveImportedModule and make HostPrepareImportedModule operation redundant and unnecessary.

Here is roughly how this could be done.

  1. Define DynamicModuleEvaluationJob(referencingScriptOrModule, specifier, promiseCapability)
    The algorithm steps are similar to TopLevelModuleEvaluationJob except that:
  • instead of calling ParseModule with the sourceText it calls HostResolveImportedModule with referencingScriptOrModule, specifier as arguments
  • the spec text explicitly states that the call to HostResolveImportedModule may perform job blocking operations and that if it does the current job is suspended until HostResolveImportedModule completes. (more below)
  • Errors or successful completion are reported by fulfilling or rejected the promise parameter.
  1. Replace step 8 of ImportCall evaluation semantics with:
  • Perform EnqueueJob("ScriptJobs", DynamicModuleEvaluationJob, « referencingScriptOrModule, specifier, promiseCapability »)
  • Return promiseCapability.[[Promise]]

We probably need to be a bit more specific about what we mean by suspending the current job if HostResolveImportedModule needs to block, as current jobs always run to completion before schedule another job. In this case what suspending means is that the current job is suspended and a new job is scheduled while HostResolveImportedModule continues to operate asynchronously. When it completes the suspended job is requeued to the ScriptsJobs queue. This probably requires to tweaking to 8.4, either adding a SuspendedJobRecord or tweaking the existing PendingJobRecord to accommodate rescheduling suspended jobs.

We might consider adding an extra parameter to HostResolveImportedModule that tells it that it is ok to asynchronously block. But I don't think that's really necessary if the spec text in DynamicModuleEvaluationJob is clear.

Finally, I considered whether we could create create a single kind of module evaluation job that combines support for both TopLevelModuleEvaluationJob and DynamicModuleEvaluationJob but concluded that the spec. will probably be clearer if they are separate.

Per spec it's not required to actually evaluate the module, which is bad

Initial thoughts on a fix:

  • Change HostFetchImportedModule to something like HostPrepareImportedModule
  • Require that HostPrepareImportedModule evaluate the module (call ModuleEvaluation? is there a state internal slot we can check?)
  • Add an assert in the import() runtime semantics that the module has been evaluated (same questions as to phrasing).

Thanks @allenwb for finding this!

need to explicitly create namespace object

In FinishDynamicImport, moduleRecord.[[Namespace]] will in most cases still be undefined. I suppose you want to explicitly call GetModuleNamespace to force the creation of the namespace object.

Synchronous version?

This request would need to be limited to dynamic import, but I think it is a real need as long as import is being visited...

Currently modules (as well as modules targeted by your proposal), being run asynchronously, do not allow modules to keep things fully modular by defining stylesheets synchronously so that any subsequent use of custom tags by users can take effect without flickering, placeholders, etc:

const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('src', 'module-specific-styling-without-flickering.css');
document.head.appendChild(link);

class CustomElementClass extends HTMLElement {...}
customElements.define('x-you-can-use-me-immediately-in-your-static-html', CustomElementClass);
export default CustomElementClass;

The same may be useful for JavaScript polyfills expressed as modules (or a series of interconnected modules) which the user may wish to import and use within the head of the document (without adding script tags for them non-modularly).

dynamic import should be an unary operator

(this is the terse form of this issue, since somehow the nice extented form got lost)

dynamic import should not be syntactically defined as a special kind of CallExpression. This deviates from the pattern used for all other keyword based expression forms including yield, await, typeof, void, delete.

However, it can't be a UnaryExpression because that precedece would be error prone:

let fooP=import base+"/foo"; //as a UnaryExpression this would parse as: (import basePath)+"/foo"

Instead it should be defined as an alternative of AssignmentExpression, just like yield:

let fooP=import base+"/foo"; //as AssignmentExpression this would parse as: import (basePath+"/foo")

Needs cancellation

The main use case I can think of for this is an instance where a component is dynamically added to a view, (classic framework route changes, for example) and the user then does something to cause the component to be removed (hits another route maybe) before the module is done arriving over the network. Ideally, we'd be able to prevent the script from being parsed if we no longer care about it anymore.

Sorry to pick at a wound. But I'm hoping you'll track this. I think import should have cancellation, (a promise is fine, just as long as we can cancel it somehow).

What happens if the argument is not a string?

It's possible I'm missing something in the spec, but it doesn't seem to be clear how import() handles cases like this:

const notAString = {};
import(notAString);

Based on the runtime semantics, it looks like HostPrepareImportedModule would get called with the non-string value as specifier. But the summary of HostPrepareImportedModule appears to imply that specifier must be a string.

Is ImportCall supposed to convert the argument into a string (resulting in [object Object] getting imported)? Or would a runtime error get thrown in that case?

Clarification on binding

I saw in #6 that some non-literal strings are accepted. Is import(someDynamicString) okay, where someDynamicString is a string referencing a valid binding?

Module namespaces as thenables: Add non-normative spec text

The conclusion to #47 was that it is indeed intentional that the promise instance's final [[PromiseResult]] returned from import():

  1. May not always be the module's namespace object
    • ...in the case where it has been "overridden" by an export named "then"
  2. May vary on each call to import() for a given specifier
    • ...in the case where the "then"-named export is not idempotent
  3. May not be accessible via the existing non-dynamic import syntax

Whilst this is well-specified intentional behavior, reviewers have seemed surprised by this discovery. The capabilities it allows were not mentioned in the original motivations or use-cases. Public educational pieces written on this proposal have not mentioned this capability. The spec wording for Runtime Semantics: HostImportModuleDynamically talks only about consistent results:

if the host environment takes the success path once for a given referencingScriptOrModule, specifier pair, it must always do so, and the same for the failure path"

To increase awareness of this behavior, I'd like to request a spec note be added to emphasize the fact that FinishDynamicImport's step 2.f means the caller of import() does not have the subjectively-intuitive equivalent guarantees of consistency.

As I am new to spec contributions, I will proposal some loosely suggested wording at the end of Runtime Semantics: HostImportModuleDynamically that I hope others may massively refine and reword:

NOTE 1: Even if the success path is consistently taken for a given referencingScriptOrModule, specifier pair, the module can still arbitrarily override the final [[PromiseState]] and [[PromiseResult]] of the promise instance returned by each import() call if the module exports a Promise executor function named "then". This is a natural consequence of FinishDynamicImport calling promiseCapability.[[Resolve]] with the module's [[Namespace]].

Please say if there is some existing policy on the conditions under which we allow/desire non-normative text to be added to the spec.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.