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

Fallback user stories #34

Open
domenic opened this issue Aug 20, 2018 · 6 comments
Open

Fallback user stories #34

domenic opened this issue Aug 20, 2018 · 6 comments
Labels
fallback semantics An issue with the proposed model for falling back from a LAPI to a polyfill

Comments

@domenic
Copy link
Collaborator

domenic commented Aug 20, 2018

I want to write up an issue to capture some foundational issues around fallbacks which have been floating around, but not consolidated. For example #33 is trying to solve these problems, without stating them, which makes it more confusing.

For the purposes of this discussion, let's assume that the fallback technology we're going to use is some form of package name maps, and that the syntax for importing standard modules is import "std:x". Neither of these assumptions are intrinsic; they're made so that we can have a more concrete discussion.

(A) No fallback needed

The web developer wants to support the following classes of web browsers:

  1. Browsers that support both package name maps and async local storage

They should be able to write import "std:async-local-storage" and get that standard library feature.

(B) Fallback for this specific LAPI

The web developer wants to support the following classes of web browsers:

  1. Browsers that support both package name maps and async local storage
  2. Browsers that support package name maps, but not async local storage

The developer has hosted a polyfill for the other supported browsers at /node_modules/als-polyfill/index.mjs.

The developer should be able to write import "std:async-local-storage" and:

  • get std:async-local-storage in class (1) browsers
  • get /node_modules/als-polyfill/index.mjs in class (2) browsers

(C) Fallback for package name map support

The web developer wants to support the following classes of web browsers:

  1. Browsers that support both package name maps and async local storage
  2. Browsers that support package name maps, but not async local storage
  3. Browsers that support neither package name maps, nor async local storage.

The developer has hosted a polyfill for the other supported browsers at /node_modules/als-polyfill/index.mjs.

The developer should be able to write import "/node_modules/als-polyfill/index.mjs" and:

  • Get std:async-local-storage in class (1) browsers
  • Get /node_modules/als-polyfill/index.mjs in class (2) and (3) browsers.

(D) Non-module browsers

In addition to the above, the web developer also wants to support browsers with no JS module support at all (i.e., non-evergreen browsers). Call these class (4) browsers.

In this case, we can assume the developer is following best practices and writing their app using the nomodule pattern:

<script type="module" src="app.mjs"></script>
<script nomodule src="bundle-from-build-tools.js"></script>
@domenic
Copy link
Collaborator Author

domenic commented Aug 20, 2018

Analysis

Story (D) is basically free if we solve story (C). Inside app.mjs, the developer acts as they would in story (C). Additionally, their build tools ensure that bundle-from-build-tools.js contains a polyfill for async local storage, and that any instances of import "/node_modules/als-polyfill/index.mjs" in their original (module-using) source code were converted into appropriate uses of that polyfill, while assembling bundle-from-build-tools.js.

Stories (B) and (C) point to a need for a new technology which remaps import statements:

  • (B) If std:async-local-storage is not implemented by the browser, treat imports of it as imports of /node_modules/als-polyfill/index.js.
  • (C) If std:async-local-storage is implemented by the browser, treat imports of /node_modules/als-polyfill/index.js as imports of std:async-local-storage.

As currently envisioned, package name maps do not solve either of these, although the adjacent concepts section mentions some ideas for solving them.

Story (A) requires no special effort and should work easily.

@domenic domenic added the fallback semantics An issue with the proposed model for falling back from a LAPI to a polyfill label Aug 20, 2018
@domenic
Copy link
Collaborator Author

domenic commented Aug 22, 2018

After talking with some folks, I realized that when I say

The developer should be able to write import "/node_modules/als-polyfill/index.mjs"

for cases (C) and (D), what I really mean is "The code shipped to the browser should contain import "/node_modules/als-polyfill/index.mjs"."

What developers write may in fact still be import "std:async-local-storage", with build tooling doing the conversion ahead of time. That's nice!

@domenic
Copy link
Collaborator Author

domenic commented Aug 22, 2018

I should also mention that in an offline chat @guybedford was suggesting a different approach, which basically lumps together user stories (C) and (D). Basically, we'd ship bundled, non-module code to both sets of browsers.

This would require some new addition to nomodule, e.g. nopackagemap. And it would cause browsers that don't implement package name maps to get the low-fidelity experience for a site for an increased amount of time, which is unfortunate. It generally makes built-in modules unusable without tools for that interim, which is especially bad. But it does make things simpler.

@littledan
Copy link

littledan commented Sep 10, 2018

Some other user stories, not sure if they are the most important, but I imagine these feature requests might come up at some point:

(E) New features for a broadly supported LAPI

The web developer wants to support the following classes of web browsers:

  1. Browsers that support both package name maps and async local storage v1.
  2. Browsers that support package name maps, but not async local storage v2.

Let's say std:async-local-storage becomes a standard, shipped to 99% of the web, and people like it so much that they develop the following (deliberately silly) new features, which end up being shipped in some browser before others:

  • The StorageArea.prototype.increment method, an atomic operation storage cell to increment the value stored at a key.
  • Support for security with SuperSecretStorageArea, which is harder to find.

I'd say #53 can handle this case very well, using a path-specific mapping which is applied in the application code which directs to a wrapper module, and within that wrapper module, the mapping is not applied.

(F) Load a different (non-polyfill) library version when built-in module not available

Like (B) in terms of supported browsers, but opposite in terms of factoring: Let's say Globalize.js wants to use the latest Intl features (becoming a thin wrapper instead of a huge bundle with lots of data), which we are (hypothetically) exposing via built-in module, but fall back to its old implementation when not supported. The solution to (B) is applicable if Globalize rewrites itself to be based on an Intl-shaped interface internally, but sometimes this translation is a bit impractical (e.g., say Intl actually supports many more features than Globalize, and it's difficult to separate out which ones will be called out to from the shim).

About (C)--I would be more worried about this transition if there weren't already such promising polyfills that aim to bridge this particular gap.

@domenic
Copy link
Collaborator Author

domenic commented Oct 3, 2018

@littledan when trying to write up an example for your use case (E), I realized that for StorageArea.prototype.increment, you can just import the module and monkeypatch it yourself. It's removing or adding exports that requires extra work.

For use case (F), I vaguely understand it, but not well enough to write up an example. I'll try to finish my draft of WICG/import-maps#53, then maybe you can help fill in an example into the ones I'm currently working on.

@littledan
Copy link

littledan commented Oct 6, 2018

@domenic Yes, your solution to (E) sounds good to me; I guess if we went with frozen built-in modules, you'd have to use subclassing or something like that. Adding exports doesn't sound so bad, given that modules can export everything from another module, and the mapping is scoped to exclude the new version. Removing is similar, just more annoying since you have to list each thing you want to re-export.

As we discussed offline, (F) can be met by top-level await. To give a concrete example, if you're currently doing feature testing like this

export let formatNumber;
if (typeof Intl.NumberFormat === "undefined") {
  formatNumber = /* Something using Intl.NumberFormat */
} else {
  formatNumber = /* from-scratch definition */
}

you could replace it to something like this, if the "next NumberFormat" comes from a built-in module:

export let formatNumber;

try {
  let { NumberFormat } = await import("@std/intl/number-format");
  formatNumber = /* Something using NumberFormat */
} catch {
  formatNumber = /* from-scratch definition */
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
fallback semantics An issue with the proposed model for falling back from a LAPI to a polyfill
Projects
None yet
Development

No branches or pull requests

2 participants