Skip to content

Instantly share code, notes, and snippets.

@brettz9
Last active December 28, 2021 02:35
Show Gist options
  • Save brettz9/cc0a44fd216c357a8561e5d06a114647 to your computer and use it in GitHub Desktop.
Save brettz9/cc0a44fd216c357a8561e5d06a114647 to your computer and use it in GitHub Desktop.

I understand your argument about force, and although I very much sympathize with the general tendency to seek to do things without what one could in a sense call "force", I think there are a few factors which are obscuring matters here.

For one, I think we have to be careful that we are not engaging in a fallacy of equivocation. While it is especially disconcerting to see "force" being used ot justify even well-intended behaviors, when understood as compulsion under threat of violence or imprisonment by the state, obviously we are not talking about anything remotely similar in degree here (assuming all would even agree that this is indeed a form of compulsion at all).

So saying "it's always wrong to force things on people" might be seen as gaining credence by attacking the strawman of a supposed claim justifying physical compulsion. This brings me to my second point which is about the polemical-sounding nature of engagement with such a fallacy.

Polemicists use ambiguous language such as this to obscure matters in the course of seeking to win temporary support for their arguments, but their doing so also obscures a detached search for truth to really look dispassionately together at what is at hand without complicating things with language that may be interpreted in a way that evokes more moral outrage than is relevant.

In this vein, to highlight the polemical impression such an argument gives from the other side, suppose I were to have used similarly equivocal language to suggest that you were in fact "forcing" others. I am not supporting this kind of polemical language, because I don't find such strong terms helpful, but it may incidentally also highlight the point about polemicism as well as the logical points about ESM breakage that I wish to make.

A polemicist might claim that you were actually the one being forceful on insisting on your viewpoint or that you were seeking to "force" the project to maintain backward compatibility. Again, this would be unfair, given the strong connotations of physical pressure of "force", but I expect you can admit, you are indeed seeking to exert some "force" in a weaker sense. True, you are not holding a gun to our head, but neither are developers of ESM-only projects. ESM-only projects are simply not doing work that you want them to continue doing.

While you might respond that there is some kind of social contract calling on us to help others, so this "burden" on us was merited, even setting aside your categorical rejection of "force" in pressuring us, still, why are we not part of the social contract too? Why are the needs of the many of us who don't want to be stuck with extra procedures not relevant? Is our free labor here a kind of slavery (yes, more potentially polemical language) whereby we are compelled to do your bidding rather than our own? This is not to mention some of us not wanting to prolong the community having no incentive to get around to supporting ESM--what about that social contract (to "think of the children" if I may further the polemicism)? If there were a technology that could speed up the Internet by 100 times, and yet it required 1 hour of pre-announced internet down-time, "forcing" users of the internet to suffer for the benefits of a new innovation, would you not be at least open to considering the idea? What if it were 1000 times the speed and only five minutes of down-time. Surely, you can see there is a spectrum here.

But my main goal is to set the polemical discussions of "force" aside, and instead get back to suggest in non-charged language that one might argue that you are putting the burden you wish to seek to avoid instead on ESM-only-favoring developers and future generations due to some absolute, unyielding value being assigned to backward compatibility.

Now backward compatibility is an understandably admirable standard to strive for, especially in fields like the web where there is such a vast prior output (and by many not so technical users) that one would not wish to lose access to it).

But another factor I would like to argue, unrelated to polemics, is that this form of "backward compatibility" is akin to the type of breakage we see as in "breaking the web". This can hardly be compared in degree to purposely creating a browser which no longer is capable of displaying old websites. We're talking about competent developers having the inconvenience of needing to tweak their code (and who regularly need to make tweaks anyways, e.g., to avoid vulnerabilities or support their userbase), and yes, perhaps face some suffering as the dependency chain is fixed or alternatives are found. But the changes required are not extreme in degree, even if it is admittedly of no small consequence either. The impossibility of contacting all website owners to "fix" their code is not the same as contacting all non-ESM projects to retool their support for ESM. In addition, most ESM projects today can continue using non-ESM code, so the burden is really only on non-ESM or polyglot dependees rather than dependents. As developers start getting used to creating ESM code, they will make ESM-friendly or polyglot versions--because they wish to support the community, and the problem can be assisted from the ground up.

@brettz9
Copy link
Author

brettz9 commented Dec 27, 2021

Yes, re: non-smoking, you are right, surely that is the general underlying reason for success either way, and I shouldn't have oversimplified it like that. But even individual business owners are not like an aggregate of amoral shareholders holding no emotions either way. They might tend to act rationally, but there are a good number of businesses, especially perhaps among the first adopters (even if not all first adopters act for such reasons) who say, "Yeah, this is something we need to do something about; I lost my mom to cancer, etc., and maybe there are people who want to avoid the smoke too", etc. (and some would have resisted the commercial benefits because they liked smoking or being open to smokers.) But still, there is a mechanism by which "force" leads to a positive outcome.

But to the main point, I'm not as qualified to speak to all the technical merits, and your arguments sound convincing, but yes, my own rationale hasn't been so much in relation to the technical merits anyways. The "superiority" solely relies in one being a single standard which has the standards-based momentum behind it to become the one way.

(The static analyzability argument in your scenario which can only be circumvented with linting does require some tooling to just work and although I personally can't bear a project without linting, I do think JavaScript should have a syntax that just works without such steps being required, but to my understanding, if CJS were a candidate for browsers, static analyzability could, I imagine, have been imposed on a subset of CJS.)

If there were a possibility on the table for browsers to support CJS and convince everyone to support that, and it could be done without the chaos it would presumably cause in confusability with bundles and such, I would have personally been fine with CJS becoming the standard instead. But unless you are trying to argue for CJS being used natively in browsers (so that there can be a different but also single standard around which the community could eventually coalesce without a build step), being told to just use browserify or webpack or whatever doesn't satisfy me for my rationales since the goal is to be both buildless as well as use a single syntax cross-environment.

So, the idea from my perspective is to reach to the benefits of being able to use just one standard. Copy paste any good, non-environment-specific JavaScript code of interest off the web, and it will just work. Import from npm and it will (eventually) just work. Simplify linting routines by not having to configure the environment for simple, poyglot scripts. That's the future I'd like to reach for such a fundamental and all-too-long delayed building block of the great, and potentially greater, JavaScript ecosystem. I fully admit it is not the most urgent need out there, but I would like us to get there, and think we can with a mix of some prodding and some gradualism. Distractions related to build steps adds a stumbling block toward new code being quickly experimented with. When I'm trying something out, in the browser or Node, at the early stages, I just want to be able to add an import statement, and write code modularly without worrying about build steps until the performance trade-off becomes noticeable. I also don't want to have to write README's with separate subsections on import methods per environment, build details, etc., so as to be helpful for newcomers. I just want to put in the effort so it can become more simple.

@ljharb
Copy link

ljharb commented Dec 27, 2021

Because dynamic import exists, ESM also requires linting to preserve static analysis.

@brettz9
Copy link
Author

brettz9 commented Dec 27, 2021

Gotcha.

@ljharb
Copy link

ljharb commented Dec 27, 2021

The utopia you want simply can’t ever happen. Browsers have a DOM, node/deno can make network requests and talk to a filesystem and have env vars etc. environments are different, and without a build process, there’s a huge amount of code that simply isn’t universal.

You always have to know what environment(s) code is written for, and you only sometimes can transform it to work in other ones.

@brettz9
Copy link
Author

brettz9 commented Dec 27, 2021

Node has JSDom, browsers can be empowered through the File Access API to talk to the file system, use node-fetch or file-fetch, reuse URLSearchParams, etc. I live almost every day in other regards in such a very comfortable utopia with various projects that are environment-independent and can leverage libraries that work in different environments. That's one of the biggest draws of JavaScript to my taste and to others I know as well. With linting especially--and I wasn't speaking against linting earlier, merely that it is ideal not to have to depend on it--and for ESM or CJS one doesn't need to depend on linting just to use it even if certain errors may get through without accurate use for a given purpose as is expected--one can prevent use of globals to ensure that any environment-specific code goes into its own entry file.

@brettz9
Copy link
Author

brettz9 commented Dec 27, 2021

And based on what I've encountered, many projects are open to becoming out-of-the-box environment-independent, but their author just happened to write it in CJS...

@ljharb
Copy link

ljharb commented Dec 27, 2021

CJS that happens not to use environment-specific features is precisely as environment-independent as ESM that happens not to, since both are well-understood input formats.

Since import maps aren't a standard, there remains no way to have node code with nonzero dependencies work in a browser without a build tool. If a build tool is required, ESM and CJS can both be identically consumed.

In other words, the only time I think you have a reasonable argument for environment-independent usage is when you have code that meets all these requirements:

  1. exists in a single file
  2. does not import or require anything
  3. does not use any environment-specific features (filesystem, network, user input, DOM)
  4. does not use any APIs outside of the main language specification, which excludes Intl - i can make an exception for console.log and setTimeout, of course, despite these not being in the language itself, since these are so universal

The amount of packages that qualify for this are vanishingly small.

@brettz9
Copy link
Author

brettz9 commented Dec 28, 2021

Though it isn't as pretty as with import maps, one can target node_modules paths directly (for a browser script tag). This works fine for the purposes of a demo when, as is the general situation in which one wishes to run a demo, it is run at the top level of an application and the relative directory structure within node_modules can therefore be deterministically known.

And since projects can make polyglot code with environment-specific code either by inline checks or, as per my preference, separate entry files (e.g., where the Node version supplies a JSDom object as the document object), one can simply import the relevant entry file, so your condition no. 3 is not required. Similarly with condition no. 4. Mitigating the challenges with this are the plethora of libraries previously mentioned like file-fetch, node-fetch, indexeddbshim, etc. which allow for reuse of the same standard APIs across browser and Node as well as browser globals implemented by Node (e.g., URL, URLSearchParams).

The demo application can thus load external third party distribution files out of node_modules (e.g., let's say a jquery distribution and plugins) and any self-contained code within the project it is demoing. The main JavaScript library being imported into the demo should not itself be hard-coding node_modules or it may have problems when imported by other packages, but the HTML file may do so. So your item no. 2 is relevant but only if the library being demoed has external dependencies. In such cases, it is often a large library which needs minification to be practical as a demo anyways which is not my use case. (I'm talking about demoing nicely, modular, single-purpose atomic packages.) But regardless, the file being imported can itself safely include imports of relative paths (e.g., if importing an ESM entry file in the source code where again there are no external dependencies), so condition 1 is not relevant.

So the only relevant condition under this frequent use case is condition 2 (if condition 2 is understood as not importing external dependencies but allowing for project-local relative imports). And hopefully import maps will help even with this condition.

@ljharb
Copy link

ljharb commented Dec 28, 2021

In order to target node_modules, you either need to serve the whole thing (a massive security risk) or run a build process to selectively serve only the files you need.

If you have separate entry files, then you also still need a build process to locate them (outside of node). Note as well that JSDOM tries, but is nowhere near sufficient or equivalent to the actual DOM.

As for node-fetch, the spec for fetch means that a non-browser implementation can never be standards-compliant, as node-fetch is also not.

I think my conditions continue to all apply.

@brettz9
Copy link
Author

brettz9 commented Dec 28, 2021

In order to target node_modules, you either need to serve the whole thing (a massive security risk) or run a build process to selectively serve only the files you need.

Not if you're running the demo locally. Again, this is intended especially for rapid development and working on smaller projects.

If you have separate entry files, then you also still need a build process to locate them (outside of node).

Or you can just discover them browsing through the file system or in the project docs.

Note as well that JSDOM tries, but is nowhere near sufficient or equivalent to the actual DOM.

True, but still useful for SSR and such.

As for node-fetch, the spec for fetch means that a non-browser implementation can never be standards-compliant, as node-fetch is also not.

I'm not sure in what regard you're speaking, but even if tweaks are necessary, one can often use an entire API unmodified besides the entry point.

@ljharb
Copy link

ljharb commented Dec 28, 2021

I'm not sure why demos matter; that's not the important audience for software. We should be optimizing for actual production usage, not one-off weekend projects.

@brettz9
Copy link
Author

brettz9 commented Dec 28, 2021

I'm not sure why demos matter; that's not the important audience for software. We should be optimizing for actual production usage, not one-off weekend projects.

Many libraries are just developer tools. They don't need a splashy public page. One can smart small, and turn it into a build routine later if desired. I feel we should also be tolerant of different purposes and levels of users in their manner of producing their work.

I personally may add a build routine later solely for copying node_modules files into the repo for use by the likes of GitHub Pages. But my project's own ESM code still doesn't have to go through a build routine in such cases (unless the main library imports other dependencies).

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