-
-
Notifications
You must be signed in to change notification settings - Fork 77
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
New: Provide ESM package for Espree #72
Conversation
@nzakas I don't believe this is a breaking change, assuming the proposed "dual package" approach is taken. |
eslint/js#457 is already marked as breaking. This is a big enough change that we need to do it in a major version. |
Currently with espree v7, if someone does If we choose named exports, as in eslint/js#458, they will be getting For example, this gives different results with acorn 7 and acorn 8: import * as acorn from "acorn";
console.log(Object.keys(acorn)); If we |
|
||
### build process | ||
|
||
Today, espree is written in commonjs (CJS) format, and a universal module definition (UMD) build is created too. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a script to create UMD, but are we publishing this bundle or using it to run any tests? If neither of those, maybe we should just drop it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can’t think of a good reason to generate UMD. I’m in favor of dropping it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what you're referring to here is the browserify step we have in the eslint repo? We use that just to create a browser-safe version of eslint for our demo and isn't an officially supported version of Espree. (You can't reference a UMD version from the Espree package.)
|
||
## Backwards Compatibility Analysis | ||
|
||
The dual package approach _should_ not break espree for anyone currently using espree. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe there will be no difference for Node apps/libs that require("espree")
, but there are some changes that might break someone else:
- We are changing the
import
API: certainly when a module thatimport
s espree is run in Node (they'll have to switch fromimport espree from "espree"
toimport * as espree from "espree"
) and maybe when the imports are interpreted by some tools (e.g., bundlers) depending on how the tool works. - Adding
exports
topackage.json
restricts modules that can be imported/required from outside.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we'd need to use import * as espree from "espree"
based on the latest Node.js compat.
The second point is valid. Just adding exports
to package.json
is a breaking change.
Adding "exports" is always a breaking change, unless you explicitly enumerate every current possible requireable entrypoint ( |
Can we specify some details in the RFC document:
|
I'm not sure what the ask is here. In my experimental PR, I did not modify the API of espree; just replaces
👍 Ideally the source is already in the format we want and we're accommodating cjs/umd with some build process. Whenever we decide to umd/cjs aren't needed, we can just drop the build step and we're good.
👍 Otherwise we end up bundling in external dependencies like In my proof-of-concept PR, I accomplished this via rollup's
Ideally we would add unit tests for importing both cjs and the esm versions of the module. I don't think it would be necessary to duplicate the existing tests for both cjs and esm, just the interface in both cases. |
It would be good to just clarify how the espree ES module can be used:
(but not |
@mdjermanovic |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a good start and a great direction to head in. I just suggested one change to the build process, but otherwise, I'm in favor.
|
||
### build process | ||
|
||
Today, espree is written in commonjs (CJS) format, and a universal module definition (UMD) build is created too. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what you're referring to here is the browserify step we have in the eslint repo? We use that just to create a browser-safe version of eslint for our demo and isn't an officially supported version of Espree. (You can't reference a UMD version from the Espree package.)
|
||
## Backwards Compatibility Analysis | ||
|
||
The dual package approach _should_ not break espree for anyone currently using espree. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we'd need to use import * as espree from "espree"
based on the latest Node.js compat.
The second point is valid. Just adding exports
to package.json
is a breaking change.
Currently, When we add It's similar to how |
Co-authored-by: Nicholas C. Zakas <nicholas@humanwhocodes.com>
… indeed a breaking change.
Yeah, I think that’s just an implementation detail that can easily be addressed. |
The main module will have named exports ( |
Yup. I think, for the purposes of this RFC, we should define how we want that to work and just accept that we'll probably have to prototype it out to make sure it works that way. I don't think we should take the prototype that preceded this RFC as requirements for how this should ultimately work. |
@mdjermanovic Yes, specifically, node v13.0 and v13.1 will only work with the string form; node v13.2-v13.6 will only work with the string form or the object form with a "default" key, and node v12.17+ and v13.7+ and v14+ work with all the features. The solution, if you need an ESM export, is something like this: "main": "./index.js"
"exports": {
".": [
{
"default": "./index.js"
},
"./index.js"
],
"./foo": [
{
"default": "./foo.js"
},
"./foo.js"
]
} The array fallback solves everything for the problematic nodes, and the similarity of "foo" to "foo.js" means pre-exports nodes will work, and "main" and the "." mean that the package name will work in both as well. Essentially, if you list the paths you want to expose, I'm happy to write (or comment on a PR, or something) the "exports" field that would handle it. |
Interesting https://blog.sindresorhus.com/get-ready-for-esm-aa53530b3f77 He's basically advocating for the pure es module solution. |
That's certainly a position, but:
|
Yes, I agree. And in the article he mentions that this strategy won't be pursued until after node 10 sunsets (mid April.) Given that we'd (hopefully) have this live in eslint well before April, dual package is still a logical choice. I was more speaking to it being genuinely interesting that someone influential is planning to go that route, not suggesting we should do that today. :) |
That author has often chosen to aggressively drop support for older environments by using modern syntax without transpiling, and as a result, many downstream users have suffered - either by being forced to switch to another (sometimes worse) dependency, or by being forced to upgrade environments sooner than required, or by passing the burden to their own users. I strongly encourage all package maintainers to avoid this pattern, influential actor or not. |
I've updated the RFC with more details from the summary @btmills provided. The only outstanding item (I think) is what to do about the ESM usage. (providing Consistent with the assumption that ESM is the future standard path and that CJS affordances are temporary, I believe it would be better to just directly use that code without a build step. The idea has been that the build step is temporary and drops away when CJS becomes unnecessary. |
@mreinstein regardless of what you prefer to think of as the final case, or the primary authoring format, the graph duplication problem is real, and thus any stateful or identity-concerned code has to be authored in CJS (since you can import CJS, but can't require ESM). |
Maybe we're talking about different things. Obviously one cannot require esm, that's the entire point of producing a dual package. You need to provide entry points for both cjs and esm cases. There is no mandate on which format the source files must be in. There's nothing stopping anyone from authoring in either format, and then producing whatever code needs to be pointed at in package.json to support both. And since either works, it just doesn't make sense to me to author in cjs since that's on the way out. |
@mreinstein right - but imagine, say, that eslint has a cache. That object needs to be shared by both CJS and ESM code in order to avoid the hazard. As such, a file exporting that object (or functions that close over it) must indeed be authored in CJS in order to be synchronously available to both module formats - even if the rest of the codebase is authored in ESM and transpiled to CJS. |
Someone else is probably a better choice to move this forward, because I don't understand the use cases and the pitfalls affecting this system. I'd suggest someone else take this over. |
If the eslint team wants to list the exact entrypoints they want, and re-clarify that bundling is a necessity, and state a preference (or lack thereof) for authoring format, i'd be happy to prepare an implementation. |
While all of this background information is super interesting (thanks @ljharb), I think we're past the point where continuing the discussion along these lines is useful to progressing this RFC. There are any number of theoretical problems with publishing a dual package. The fact of the matter is that ESLint is the primary consumer of Espree, and we don't guarantee Espree for use in any other way, so most of the problems are unlikely to occur, and if they do, it will be our own fault (and we can fix them). So in the spirit of moving this forward, I'm fine with shipping ESM source code as-is and then compiling a CJS file in dist for publishing. That's what @mreinstein originally proposed and we originally liked the idea of, so I've changed my mind and would like to let him run with it. If there end up being issues, it will be easier to solve them when the problem is concrete rather than trying to solve it in the abstract. @mreinstein does that sound okay with you? |
It sounds fine to me, but I'd like to put a little more color on my previous statement. I didn't mean to imply "it has to be my way or I'm taking my toys and going home." I'm coming from a place of being genuinely concerned about probably not understanding this particular module's use case enough, and that it might make more sense for someone that has that knowledge to be driving things. |
Sounds good. Still happy to help craft a maximally compatible "exports" field as needed. |
I will gladly take you up on that :-D |
@mreinstein no worries at all. I'm not sure anyone really understands this all that well, which is why there's been so much discussion. Do you feel like the RFC currently represents your best and complete take on the approach? |
I think so, but I'm happy to add more details to summarize what's been discussed in here if anyone thinks there are gaps. I very loosely addressed them in PR updates @ 1am this morning so I was even more prone to miss things than usual. :) |
there is a known issue related to webpack-bundled eslint: eslint/eslint#13974 it could also happen if we bundle eslint with rollup; but for espree, it's not an issue. |
Moving into final commenting as it seems we have our initial direction set at this point. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. This accurately reflects both the current state and where we want to go, and it answers all the discussion points. Thanks for all your work on this, @mreinstein. I'm excited to see this merged!
│ (ESM entry point) ├───BUILD_STEP───┘ ESM ---▶ "import": "espree.js", | ||
│ │ CJS ---▶ "require": "./dist/espree.cjs" | ||
└-------------------┘ }, | ||
"type": "module" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type module is not required whatsoever; .mjs
files are always ESM and .cjs
files are always CJS, and type module makes .js
mean ESM instead of CJS. for back compat (as well as ideological correctness), however, you actively do not want to change how .js
files are parsed, so a dual package should either omit type
or set it to "commonjs"
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for back compat (as well as ideological correctness), however, you actively do not want to change how
.js
files are parsed, so a dual package should either omittype
or set it to `"commonjs"
dist/espree.cjs
will be the only CJS module we publish in v8. All .js
files we're publishing as CJS in v7 will become ESM in v8, so we want them to be treated as ESM.
Not sure what's the plan for the files we use for development, like tests and tools.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, but why not publish ESM as .mjs
, rather than needing to change what .js
has always meant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rather than needing to change what .js has always meant?
I mean we're getting into opinion territory for sure, but I suspect that .mjs
will eventually looked back upon as "oh yeah, that was that weird file extension proposal that node and google were advocating, back when the module ecosystem was super fragmented".
I think it's inevitable that what .js
means is changing, that in the somewhat near future it will be assumed to be esm, and not the other way around.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That may be true in the future, but that is not true now, and it seems like eslint should match the present instead of a possible future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That may be true in the future, but that is not true now
I think that's a fair point 👍
eslint should match the present instead of a possible future.
I think it's a bit of a balancing act. It obviously needs to match present needs and expectations, but it also needs to prepare for a future that is coming pretty soon. esm has already shipped in browsers, in node, in deno.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but browsers have no concept of extensions at all, and node is highly unlikely to ever change the default parsing behavior, so there's really no downside in sticking to the status quo for what extensions mean.
"type": "module" | ||
``` | ||
|
||
browserify is specifically oriented around commonjs as the input format, so I also propose replacing it with rollup, which understands and expects ESM as it's input format. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'd be careful about this; rollup tends to violate the spec in various ways. browserify works just fine with ESM as far as i'm aware.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We were previously only using browserify for a long-dead demo on the website. eslint/js#466 removes it. What spec violations should we be on the lookout for with rollup?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have any current ones in mind; but for one, its entire heuristic for removing dead code is flawed - it will remove things that can throw exceptions, which broke es5-shim on airbnb.com for many months, which was relying on those exceptions being triggered. They fixed this by detecting the specific cases es5-shim is using, but that doesn't fix the root problem. For another, when import * as
can't be statically detected with certainty that some imports are unused, rather than keeping them all or erroring, they opt to drop them, so if you're doing anything fancy with module namespace objects, things can break.
I'm sure issues will be addressed as they're found, but the overall approach and ideology of the project keeps me very wary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sure issues will be addressed as they're found, but the overall approach and ideology of the project keeps me very wary.
The only thing we're really using any bundler for is changing the import
to require
and export
to module.exports
for the cjs build. I can't imagine there will be much spec violation if that's all we're doing?
browserify works just fine with ESM
huh, that's odd. My understanding is esm still is unsupported in browserify. Was looking to see if this has landed and it's still on the wishlist: browserify/browserify#1186
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify, it works with ESM in concert with babel.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll bundle only our own code, so we probably don't need dead code removal. Can we turn it off with treeshake: false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure tree-shake is opt-in only; it's been experimental for as long as I know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the docs for the treeshake option says Default: true
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh those crazy bastards enabled it by default now! :D ok, then yeah we should definitely shut off tree-shake.
I think we are at the point where we understand the overall direction. There are probably some details that will be easier to work out with code than opinion, so I think it’s safe to merge this. Any objections? (To clarify: the RFC process is not intended to cover every edge case, it’s to describe and approve a general direction to head in.) |
Okay, let's get to work on this thing. :) |
* Build: generate es and cjs modules. Fixes #457 * Build: add note explaining lib/version.js and lint * Build: update unit and lint testing commands * Update .eslintrc.cjs Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update package.json Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Chore: add commonjs unit tests and lint (fixes #469) * Chore: fix acorn unit test and lint (fixes #469) * Build: remove node 10.x from the test matrix (fixes #469) * Build: add commonjs build step for CI (fixes #469) * Build: add node 10.x test as separate job * Breaking: acorn 8.0.5 * Build: remove unnecessary tests * Build: disable rollup treeshake * Update package.json Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/commonjs.cjs Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/commonjs.cjs Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/commonjs.cjs Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Build: fix linting and remove unneeded ignore file * Update tests/lib/commonjs.cjs Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Build: update eslint config * Chore: replace String.indexOf with String.includes * Chore: use pathToFileURL to hopefully fix esm based tests on windows * Chore: use pathToFileURL to hopefully fix esm based tests on windows * Chore: use pathToFileURL to hopefully fix esm based tests on windows * replace tap with mocha and minor dep updates * Fix Node 15 peer deps issue * Update package.json Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * fix linting problems * add basic jsx test for commonjs build Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
Summary
Provide an ecmascript module for espree.
Related Issues
eslint/js#457