-
Notifications
You must be signed in to change notification settings - Fork 28
🚀 Feature: Enforce order of imports
and exports
#928
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
Comments
Thanks for submitting this and thanks especially for all of the detail. I am a bit concerned about the amount of complexity that's involved (which could be completely merited). I'm a big fan of starting small and iterating. So, I'm wondering if we should maybe start with a smaller subset of this proposal; release that, see how it works, get feedback, and then continue with the rest if it still makes sense to. If we did go that route, then what would be that MVP? I think a good MVP could be
That leaves the different categorization and rules of groupings out for the MVP, and something that could be built on as a follow-up iteration after seeing how it works in the wild. @JoshuaKGoldberg thoughts? |
In the friendliest of tones: tl;dr. 🙂 I don't have the bandwidth to pour over and deeply understand the proposal of this complexity and nuance. Agreed that it is not warranted for the problem of this scale or at this point. Requesting a proposal that fits in <500 words. |
@michaelfaith Thats a good instinct and I would approach it similarly. The only modification I would make is to ignore the @JoshuaKGoldberg sorry 🫣 lol I will sometimes over-invest in the research and ideation phase - and majority of the text is justifying the ideas, to show I'm not just making arbitrary decisions lol but the intention is not to suggest that all is done in one go - but to come up with a complete (or close to complete) vision that we can break up into parts that make sense to introduce in isolation. Is it okay if we use this issue to track all of the features but use small issues for each individual part? Since everything relates to the same issue of sorting the |
Dropping |
@michaelfaith sorry that was a weird way to say that 😄 . I mean, instead of working on the |
Gotcha. The main reason I left those out in my recommendation was because that starts to delve into the categorizations (i.e. what belongs with what). But if that can be introduced in a simple way without the additional overhead of the categories, and is extensible so that more can be added to it, then I could get behind that. |
Bug Report Checklist
main
branch of the repository.Overview
This is a feature request and proposal for handling the
exports
andimports
keysAdditional Info
Context
At the moment, the
sort-collections
rule improperly enforces alphabetical order on top-level condition objects in theexports
andimports
(if specified) keys alphabetically. Sorting condition objects alphabetically causes issue because resolvers that support import/export conditions use the first matching condition. This is especially bad when running the Eslint auto-fixer, which can introduce production issues for the consumers of the package.In addition, enforcing a consistent order of the condition objects has a high-impact potential for teams maintaining large amounts of package.json files, supporting multiple code variants (syntaxes, environments, runtimes, etc). The more conditions you support, the easier it is to accidentally create a dead-code path (a condition that will never be met) and to accidentally break a consumer that depends on a particular set of conditional exports.
Proposal
eslint-plugin-package-json
should enforce a consistent order of theexports
andimports
keys,It should implement an order of the conditions object based on openly available knowledge of "known" conditions while allowing developers to specify custom conditions. In addition, it should sort the subpath objects to make them more readable.
Before explaining the proposal, I'll leave a simple description of the terms I will use in the rest of the proposal.
Terms
subpath and condition objects
Subpath exports objects are the objects that describe the package entry-points, where the keys start with
.
.Subpath import objects are the objects that describe the private path aliases, where the keys start with
#
.Condition objects describe the conditions supported by the entrypoint, where the keys do not start with
.
or#
.dead-code paths
Conditions in the condition object that will never be met.
Subpath Object Order
The Subpath Object is the easier part of this feature, so I will detail it first.
Goals
The goal is to make these objects more readable. I want to enforce an order that reads naturally and resembles a logical order when using wildcards (
*
).Solution
*
) to move them after concrete subpaths.Examples
Invalid
Valid
Condition Object Order
The condition object should be organized to avoid common problems.
Goals
The primary goal of the condition object order is to minimize dead-code paths. Dead-code paths are created when more than one condition is met by the resolver, but a more specific condition is nullified by a less specific condition.
The simplest example of this is when using the
default
condition before any other condition. Thedefault
condition is the ultimate fallback condition, so every resolver will match it regardless of any other matching conditionExample:
A more nuanced example is the following:
In the last example, the
browser
condition may be a dead-code path. Thebrowser
condition is generally used by bundlers (like Webpack, Rollup, etc), but they generally also use theimport
condition. So even if the bundle is targeting browsers, thebrowser
condition will not be used.Some condition pairs have a strict order requirement. For example,
module
must come beforeimport
,import
must come beforemodule-sync
,module-sync
must come beforerequire
- the relative order of these conditions should be enforced by our solution.In some cases, there isn't a strict order requirement between known conditions, however, the other goal of the proposed condition order is to group similar conditions together.
For example, the
rollup
condition used by the Rollup bundler and thewebpack
condition used by Webpack don't have any overlapping scenarios, so their relative order does not matter; However, it would be nice to have these two next to each other as they are both conditions for bundlers.Constraints
In my research, I've found 40 well-known conditions that are used by various open source tools and services, so the biggest challenge I've found is creating a widely applicable model to apply to each condition when determining their relative order.
I've concluded that it is impossible (or very difficult) to create a unified model for all of the conditions, as there are many configurations that produce valid packages without dead-code paths.
Take for example the
require
andbrowser
conditions.require
is used by resolvers looking for a CJS module andbrowser
is used by resolvers in bundlers when targeting browsers.Both of the following configurations are valid:
In the first path (
./browser-first
) a bundler may use thebrowser
condition if targeting a browser, but will use therequire
condition when targeting Node.js. Thenode
CLI will also use therequire
condition.In the second path (
./require-first
) a bundler that does not supportrequire
will use thebrowser
condition if it is configured to target a browser. However, Node.js or a bundler configured to support commonjs will use therequire
condition.This is to say, there isn't a way to globally enforce a relative order between these conditions. But it is still too easy to accidentally create dead-code paths.
Solution
My proposed solution to reduce the risk of dead-code paths while enforcing a correct order is multi-faceted.
1. Enforce categorized condition objects
To avoid accidental dead-code paths, we can add a rule that limits the use overlapping conditions in the same object - forcing developers to choose mutually exclusive conditions. The mutually exclusive conditions can be defined as condition groups, which can be extended if necessary.
Example:
Configuration example
This configuration communicates that you can only use
browser
andnode
in the same object, andmodule
,require
andimport
in the same object, but you cannot mix-match between groups. In addition, the order of the condition name arrays should be the enforced key order within the condition object. Somodule
must come beforerequire
, etc.2. Fallback conditions and sorting exceptions
The treatment of condition categorization should not apply to a list of known conditions that can be considered fallback conditions.
default
conditionThe
default
condition can be added to any condition object and must always be at the end.types
andtypes@{typesVersionsSelector}
The
types
andtypes@{typesVersionsSelector}
conditions can also be added to any condition object and should generally be at the top (though there is some flexibility with this because of how typescript works).The
types@{typesVersionsSelector}
condition should come before thetypes
condition and should be sorted using a version range sorting algorithm.Handling this scenario will require a non-trivial solution to parse the version ranges and sort them based on specificity.
3. Provide default groups
Most conditions used in the wild are well known conditions with well established semantics and relationships with other conditions. This can be captured in the rule's default configurations. These are the groups and conditions I've identified in my research:
Environment
keys:
test
development
production
Bundler
keys:
vite
rollup
webpack
Edge-Runtime
keys:
azion
(By Azion)edge-light
(By Vercel)edge-routine
(By Alibaba Cloud)fastly
(By Fastly)lagon
(By Lagon [Now Vercel])netlify
(By Netlifywasmer
(By Wasmer)workerd
(By Cloudflare)Server Variant
keys:
react-server
Target Runtime
keys:
macro
bun
deno
browser
electron
kiesel
node-addons
node
moddable
react-native
worker
worklet
Reference Syntax
keys:
svelte
asset
sass
stylus
style
script
module
import
module-sync
require
4. Support for unknown conditions
It's not possible to predict all the possible conditions that will exist in the future or conditions that are project specific. In addition, we can't predict the relationship between those conditions and well established/known conditions.
We can approach this by restricting the use of unknown conditions by default, but giving developers the choice to configure the rule, to add their own
conditionGroups
or extend the default conditions in the predefined groups.Example configuration:
Additional requirements
Drawbacks
These are the drawbacks I've identified. I may edit this list as we discover more drawbacks.
More verbose export objects.
Developers will be forced to create more verbose objects. This can be tedious in small/simple projects. In larger projects, this is offset by the benefit of clarify.
Example
Will have to be written as
or
Resources
I've used the following resources to come up with this proposal
The text was updated successfully, but these errors were encountered: