-
Notifications
You must be signed in to change notification settings - Fork 44
Universal normalization of exports / alternative to nested conditional exports #453
Comments
I think the normalization should drop unknown conditions. Because it feels weird if the normalized form has things that are just ignored. Example of what I mean: {
"systemx": ["std:so", "std:many", "std:things", "std:to", "std:check"]
}
// Normalize on node to:
[
// nothing to do
] Benefit: It makes these a lot more helpful in error messages and debugging. Because node never really tried to check for |
My idea of a normalized pattern would be: type Conditional = { flag: string, value: any, path: Candidate };
type Candidate = Conditional | string;
type ExportMapping = Candidate[]; That way the normalized form has exactly one representation. E.g. the only array is at the top level. And I don't think the normalized form should know that node is a platform. It should just have a clear list of prioritized conditions. Object order in |
@jkrems My complaint with your suggested normalized pattern is Personally I still do not like the nested structure. My issues with nested are potential confusion about greedy vs non-greedy and the result of Take the following example: {
"node": {
"require": "./index.cjs"
},
"default": "./index.mjs"
} Without reading documentation I would intuitively expect that this says {
"node": {
"require": "./index.cjs",
"default": null
},
"default": "./index.mjs"
} Supporting |
If you don't like the type Conditional = { conditions: string[], path: Candidate };
type ExportMapping = Conditional[]; Taking your example and non-greedy semantics: {
"node": {
"require": "./index.cjs"
},
"default": "./index.mjs"
}
// Normalized to:
[
{ conditions: ['node', 'require'], path: './index.cjs' },
{ conditions: [], path: './index.mjs' },
]
This feels orthogonal to the normalized form. It sounds like you're saying "I think non-greedy is more intuitive" which I can definitely understand. For completeness-sake, we could also express greedy matching in a normalized form by doing something like this: [
{ conditions: ['node', 'require'], path: './index.cjs' },
{ conditions: ['node', 'require'], path: null }, // abort search here
{ conditions: [], path: './index.mjs' },
] But it feels a bit confusing because it's really hard to distinguish between "none of the files is applicable for this condition" and "this condition wants to explicitly block any kind of import if it applies". |
100% for me.
Did you mean to have the abort entry say |
It’s certainly not, and that’s open for discussion. (Not that I’m saying that I oppose it, but its complexity makes me concerned.) As I understand it, and @guybedford or @jkrems correct me please, we have/need nesting because of sugars. For example, we could allow only the most verbose syntax: "exports": {
".": {
"require": "./index.cjs",
"import": "./index.mjs"
}
} But because most packages have only one export, Guy wanted to mimic the non-conditional "exports": {
"require": "./index.cjs",
"import": "./index.mjs"
} And because Jan wanted to have explicit prioritization, we also have the array form. |
I simplified but I guess too much. The complete version would be something like this - at the end of each branch is an abort ("at the closing brace"): [
{ conditions: ['node', 'require'], path: './index.cjs' },
{ conditions: ['node', 'require'], path: null }, // abort search here
{ conditions: ['node'], path: null }, // abort search here
{ conditions: [], path: './index.mjs' },
{ conditions: [], path: null }, // abort search here
] |
I don't think I agree. :) We have nesting because a single condition isn't enough to express all use cases we found. Examples:
The design goal was to explicitly not just solve for the basic "require vs. import and only works in node" case. |
@guybedford Just to clarify I'm not questioning |
Considering exports now use object order I think it's possible to create a normalization function that does not depend on knowledge of any specific loader. Also IMO the time for changes to conditional exports has passed so I'm closing this. |
Sorry for bringing this up late in the process but #452 (comment) made me think about things and I didn't want to create a tangent in that thread.
I'd like us to consider what it would take to describe an algorithm for normalizing exports so each export could be represented by an array with one condition per array element. This algorithm would require that condition priority is object-order rather than resolver defined so the normalization could be universal (a specific resolver cannot define the priority of unknown keys).
Take the following:
The nested structure raises an issue - is
systemx
a platform or a condition? An alternative which avoids nested conditionals and eliminates ambiguity ofsystemx
:In this example conditional keys are
@platform
,@platform/condition
orcondition
. Characters@
and/
are reserved so not allowed as part of platform or condition identifiers. I use@
and/
just for demonstration purposes, this can be changed if it would cause confusion over scoped package names.The normalization of of the exports object could be:
In addition to allowing creation of a universal normalization (and possibly resolution search) algorithm this would reduce the potential complexity/depth of the
package.json#exports
structure. This would be especially nice consideringnpm
will force line-feeds intopackage.json
any time it makes a change, object order would allow users to generally avoid array notation.This proposal would rule out greedy-matching as nesting would not exist. For blocking of fallback I think this could be accomplished by using a
false
target:This would specify that
require()
in node would resolvenode.cjs
, trying toimport()
on node would fail as thefalse
would prevent node from seeing the default condition.The text was updated successfully, but these errors were encountered: