Weak imports

This is a follow-up of this blog post and this Mastodon discussion with @awbjs .

I would like to propose imports that only resolve iff there is at least one strong (=non-weak) import to the same module specifier, otherwise all their imported references are null.

The rationale is explained at length in the blog post, but in a nutshell: Libraries often need to be able to "enhance" their behavior if certain other dependencies are loaded, but can function without these dependencies. Right now it's all-or-nothing: either you import a dependency yourself and use it (and accept its contribution to the size of your dependencies), or you don't use it at all, or you expand your API surface and your users' cognitive overhead with special methods that they can use to connect a library they imported with your library.

Syntax-wise, @awbjs suggested piggybacking on the import assertions proposal. Possibly this could be done through a Boolean weak property. I’m not sure if it's possible to have imports fail differently (null instead of throwing) based on the values of import assertions, but this would combine nicely with the optional imports proposal.

For bare module specifiers, there's usually only one way to reference a given module, but when importing from URLs, there may often be several different URLs to refer to the same module.
Therefore, it may be good to allow several comma-separated specifiers in that syntax that will be tested in order, though this rules out import assertions, unless "specifier stacks" became a general part of ImportDeclaration (which could be more broadly useful, but much less for strong imports).

Weak imports would be useful even without this, as it can be done manually by the author:

import? foo1 from "https://cdn.jsdelivr.net/npm/foo" assert { weak: true };
import? foo2 from "https://unpkg.com/foo" assert { weak: true };

const foo = foo1 ?? foo2;

Versioning is the obvious counterargument to this. How can an import be useful if you could get any version back?
Authors would need to be careful to check version numbers or feature detects, since they may get a different version of the module than they expect.
But this is no different than using built-in browser APIs.
They could even have multiple weak imports for multiple versioned URLs, and then they know which version they got.

1 Like

What if the package you're trying to weekly import does exist, but it's just not at the location you're looking for it at? e.g. maybe you want to optionally do extra stuff is, I dunno, jQuery is loaded up, but the consumer of your library might not be loading jQuery from the CDN, they might have downloaded and bundled the library with their app.

I'm not really a fan of this sort of "spooky action", where making module A import module B can cause changes to the behavior of module C.

Take an example you give in the blog post: "if you’ve loaded a parser, you’d prefer it over parsing with regexes". So now if I start using this parser in one part of my application, a completely unrelated part of the application is going to switch out its underlying mechanisms? How likely is it that the two ways of parsing have identical behavior (including error messages)? Not very likely. So there mere act of importing this module is going to cause changes (and therefore probably breakages) to an unrelated component. You really, really don't want to have non-local effects like this.

Yes, passing in libraries as parameters is a little bit clunky, but it's much better than this alternative.

1 Like

Then you just don’t get the enhancement and all is still fine :blush:
As discussed in the OP specifier stacks help mitigate this, and it’s more rare with bare specifiers, but of course it will be common.

Do note that the alternative of having API surface so that the consumer of the library can do the connection also has the risk of the consumer of the library simply not knowing about this, or being unaware that a certain module is loaded as it’s deep in another module’s dependencies.

Correct me if I'm wrong, but by specifier stacks, you mean something like this?

import jQuery from 'https://cdn1/jQuery/v2.1.1,https://cdn2/jQuery/v2.1.1';

It tries to import from the first URL, if that fails it goes to the second, and so on?

And, if your library is compatible with, say, 100 different releases of jQuery (most of which are various patch releases), and you want to support, say, 5 different CDNs that host jQuery, you're going to be listing so many URLs in that import line that you probably could inline a tree-shaken version of jQuery in the same number of bytes :). And you still wouldn't be covering the case where I just downloaded the library as a file, named it whatever I want (jQuery.js, jQuery_1_2_3.js, jQuery.1.2.3.js, jQuery.1.2.3.min.js, etc), and imported it using a relative URL. And, even if you did cover those cases, what would happen if my downloaded copy of jQuery is actually a forked version of the library, with severe differences in functionality?

I know this is putting the issue into a worst-case-scenario, but that does outline my concern - that there's really no way to easily tell, just by the import statement alone, which library you're importing.

I also share @bakkot's concern. Say I make the library "coolLibrary.js" and I depend on "awesomeDependency.js". You produce library "fragileLibrary.js" and want to optionally depend on "awesomeDependency.js" to provide additional functionality. A UI is built that uses both "coolLibrary.js" and "fragileLibrary.js", and since "coolLibrary.js" automatically loads "awesomeDependency.js", "fragileLibrary.js"'s additional features are auto-turned on. The UI starts to accidentally depend on those additional features, without realizing that the only reason they're on, is because they're using "coolLibrary.js" which in turn uses "awesomeDependency.js". Now, "coolLibrary.js" makes, what's supposed to be, an internal-only modification (something that shouldn't be observable by users of the library) - they drop their dependency on "awesomeDependency.js", or maybe they simply switch which CDN they import it from. All of a sudden, when the UI we're talking about does a patch-update of "coolLibrary.js", the extra features that "fragileLibrary.js" provided get auto-turned off, and all sorts of broken code results from it.

Wow, that's a mouthful, but hopefully that gets my thoughts across.

Perhaps, this sort of feature is better suited for a package manager, like NPM to implement

Think, something like this:

You install package somePackage.

When somePackage installs, it asks you if you want to enable feature "coolFeature" at the cost of X bytes. Alternatively, if you happen to already be depending, either directly, or indirectly, on the library it wants, it'll ask you if you want to enable it, and it'll explain that, because dependency A depends on B, depends on C, and coolFeature happens to need C, you can currently get this extra feature for zero extra bytes.

If you enable the feature, then the package manager will guarantee that the particular dependency will always be available, to keep that feature working.

A library who has these sorts of optional dependencies can declare them in their package.json (or, wherever), and declare which features would be enabled if these dependencies are provided.

Inside that package's source code, you would, perhaps import the optional dependency like this:

import libraryOrNull from 'optional:someLibrary';

This would solve all of the issues:

  1. There's an easy way to know if the library you want to optionally depend on is already being imported. This is precisely what package managers do - they figure out who depends on what, and wires the dependencies together, so that everyone's not trying to embed their own copies of that dependency.

  2. We don't have any more surprise effects automatically happening whenever we install, upgrade, or uninstall a library. The package manager will make sure that if you've requested certain features, that the needed dependencies will exist to make that happen.

Correct me if I'm wrong, but by specifier stacks, you mean something like this?

I posted an entirely separate proposal about this, check it out here: Import specifier stacks, for fallback specifiers

Yours is cleaner with more JS like syntax.

I converted my projects now to use import statements over require, but there's a better design pattern they could've picked which is more JS like.