Skip to content

ES modules #3613

@hdgarrood

Description

@hdgarrood

Previously: #2207, #2558, #2574, #2575, #3427. I'm making a new issue because the discussions in many of the linked issues are quite long, and because the ES modules landscape has changed quite a lot since when most of the discussion happened two years ago (in particular, all major browsers now support ES modules). Also, we should probably implement emitting ES modules at the same time as switching to ES modules for FFI, so I think all of these things should be considered together.

ES modules provide a number of benefits over CommonJS; most notably easier dead code elimination and wide browser support (see https://caniuse.com/#feat=es6-module). Having the compiler use and understand ES modules natively would mean that many workflows wouldn't require bundling at all, which would significantly speed up and simplify things, especially during development.

It should remain possible to run a PureScript-only app in modern browsers and in Node.js with just purs, i.e. without having to bring in external JS tools such as webpack. Currently this is achieved by invoking purs compile and then purs bundle (or just purs compile if you only want to run on Node.js).

I think the following is relatively uncontroversial, and is what we generally have settled on in the above linked issues:

  • Compile import declarations in PureScript to ES module import statements, so that import Prelude compiles to something like import * as Prelude from "../Prelude/index.js". Note that import Prelude currently compiles to var Prelude = require("../Prelude/index.js").
  • Compile exports as ES module exports, perhaps export { apply, identity } rather than the current module.exports = { apply: apply, identity: identity }
  • Mangle primes in imports and exports in both regular PureScript modules and in FFI modules, so for example if a PureScript module exports an identifier foo', then the compiled ES module exports it as foo$prime. In particular, note that writing foreign import foo' :: Int in a PureScript file means that the compiler should look for foo$prime in the corresponding FFI module.
  • Support writing FFI modules both as ES modules and with the current CommonJS format, at least for a period of time.

There are some slightly harder questions to be resolved about how we approach FFI files still. The only part of the compiler which needs to care about the format of FFI modules is purs bundle of course, but we should also consider other workflows (i.e. bundling with tools like rollup or webpack, or running directly in Node.js). For example: what file extension should we use? I'd prefer to avoid .mjs and .cjs if we possibly can, as they're only a temporary Node.js thing, and lots of existing tools and scripts will expect to find things under e.g. output/*/*.js. In fact I think if we were using .js files everywhere, including for FFI modules (which could be a mixture of ES modules and CommonJS), the only setup where we might have problems would be apps running in Node.js with no bundling step, and perhaps that's not too much of a problem: if you're using Node.js there's a good chance you are already using webpack or babel or something similar anyway, but if not it's probably not too difficult to set one up, either with one of the JS bundling tools or with purs bundle.

I don't think we should have people use the .mjs extension for FFI files which are written as ES modules; instead, I'd prefer to always use the .js extension and always parse FFI modules in strict mode / module mode, as I expect the proportion of FFI modules which would parse differently in strict mode is very small and possibly even zero (most include "use strict"; directives already). We can then handle the syntax trees of both CommonJS modules and ES modules after parsing.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions