-
Notifications
You must be signed in to change notification settings - Fork 30.2k
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
"Exports" Field of Package.json Violates JSON Language Spec #37777
Comments
The good news is that changing this behavior introduces no breaking changes for users. Every manually-written package.json file will still work just as it did before. Plus, there will be one fewer foot-gun in node that requires a long explanation in the docs! |
@nodejs/modules |
Finally, I have seen "custom" entries in EITHER OR
This provides user-control over the order for weird edge cases while still adhering to the JSON spec. This same "string OR array" approach could be applied to the other properties as well, if needed. |
|
Yes, if we check the ECMAScript specification it points to ECMA-404 to define JSON. Under "Normative References":
ECMA-404 https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf So I am pretty convinced the spec is clear that we are allowed to assign significance to object key value without violating the spec we are implementing. (I am not saying we should or whether or not it's a good idea) |
IMHO it would be worth considering if we can tweak the resolution algorithm to make the ordering optional. If someone wants to work on this, the relevant code is here: node/lib/internal/modules/esm/resolve.js Lines 453 to 475 in d0a92e2
|
The ordering is an important part of the design; given that package.json is matching node/v8's implementation precisely, it doesn't have to be interoperable - it only has to work reliably in node, which it does. |
I'm not sure how that's accurate - a JSON library that does Here's the node repl: > JSON.stringify({ default: 1, node: 2 })
'{"default":1,"node":2}'
> JSON.stringify({ node: 2, default: 1 })
'{"node":2,"default":1}'
> JSON.parse('{"default":1,"node":2}')
{ default: 1, node: 2 }
> JSON.parse('{"node":2,"default":1}')
{ node: 2, default: 1 } My JSON library, for example, doesn't suffer from this problem as far as I can tell: https://npmjs.com/json-file-plus |
While JSON itself does not impose meaning to the ordering of fields within an object, applications using JSON are free to do so, and it's not a violation of the spec in any way. |
I'd be -1 to this since as @ljharb points out this is a breaking change that would violate some of the design workflows for "exports". In addition the ordering of the CLI flag |
A few thoughts:
|
I am unclear on where the ordering is specified in the docs; it shouldn't be since in https://nodejs.org/dist/latest-v14.x/docs/api/packages.html#packages_conditional_exports it is stated: ""Within the "exports" object, key order is significant. During condition matching, earlier entries have higher priority and take precedence over later entries.""
Some of it is for human convenience, some of it is to disallow mis-ordering between tools (and features like For human convenience, specifying as an array was looked at in the issue linked above. The array is meaningless except to have an ordering of matching which as pointed out above is already provided in JS and in a few other situations in other languages (such as requiring type tags be at the start in some marshaling libraries). Per my personal preference and likely that of others hoping for easy DX in Node, that would be nice to have human convenience if it is not sufficiently inconvenient for a machine to handle. To disallow mis-ordering, keeping the ordering in the schema itself was the only real way to do this. Tooling could diverge, add new conditions, etc. if the ordering was not in the JSON itself. This starts to lean back towards convenience. Complex workflows are also hard to deal w/ and introduce some backtracking if we start using something like an array: {
"exports": {".": [
{"node": {"require": "a"}},
{"import": "b"}
]}
} Currently, there is no backtracking to allow for cases where you don't want fall through. In the example above, without backtracking you can prevent someone from using |
@bmeck I still think this is a fragile design that implicitly ties package.json files to Javascript tooling only. However, to your credit, you’ve explained the details behind the choice well. I’ll file an enhancement request with Apple Engineering to see if Swift’s JSON libraries can be updated to handle ordered property values. Someone should get around to responding in 3-7 years. The response will be “no”. |
@bdkjones, thank you for filing this issue. You bring up valid points. I have wanted to know the name of the data structure that preserves key order for this very reason. This is surely not your average JS |
These kinds of data structures are generally called Ordered Maps. There are a variety of ways to order maps, this particular one is in insertion order. |
@DerekNonGeneric Swift does have a class called “KeyValuePairs” that maintains order. The trouble is that the JSON libraries in Swift do not accept that class. They translate JSON objects to Dictionary types and the order of keys is random in a Dictionary. To support what node is doing with While it’s clear that node’s behavior is not a technical violation of the JSON spec, it does rely on what is, for all practical purposes, non-standard behavior. It’s just that the motivation to do anything about it is low because, as others have pointed out, this isn’t an issue for Javascript-based tools. It’s only an issue for other languages. |
Filing an enhancement request with Apple Engineering to see if Swift’s JSON libraries can be updated to use |
@derek-findthebrink I already know what they’ll say. In 99.9% of cases, Dictionary is the better choice. |
In general if you want a performant insertion order list you don't iterate key value pairs, Map for example in JS is ordered the same as Object properties. Keeping a separate table from your hash of the ordering and only messing with it based upon insertion/deletion is pretty snappy. |
Map is insertion order and doesn't do the weird object properties "is this a numberic uint32-1 property or symbol" dance IIRC. |
FWIW I think we could make the algorithm accept nested arrays without being a breaking change: {
"name": "name",
"exports": [
[ "require", "node", "default"],
["./cjs.cjs", "./node.mjs", "./browser.mjs"]
]
} This structure is easily turned back into an if(Array.isArray(exports) && Array.isArray(exports[0]) && Array.isArray(exports[1])) {
exports = Object.fromEntries(exports);
} Of course I'm not saying we should do that, but I think that's a solution worth considering if keeping the order of keys is a big hassle outside of JS. |
To be a bit more concrete here: It's an issue for some older versions of Python and, now newly discovered, Swift/Obj-C. The popular JSON implementations across most languages either preserve insertion order or at least have a simple option to do it. When only looking at current versions, Swift/Obj-C is the odd one out that doesn't behave like other JSON implementations. And, as others have pointed out, that may means that it's arguably not a complete JSON implementation because the JSON spec does specify an order, kind of. We already allow people to create exports fields that are 100% safe in the face of object key reordering: {
"name": "name",
"exports": [
[{"require": "./cjs.cjs"}],
[{"node": "./node.mjs"}],
// or just "./browser.mjs" because a string value is implicitly "default"
[{"default": "./browser.mjs"}]
]
} So if somebody wants to use Swift to format their |
I say we close as resolved? |
@jkrems the issue isn't when I generate package.json files; it's when I consume them. Nobody structures them as you've proposed, which prevents Swift-based frameworks from parsing these files without potentially losing information. (Although, semantically, what you propose above would have been the "correct" way to do this had it been enforced from the start. Now though, that ship has sailed and everyone omits the arrays.) This isn't just old Python and Swift. Just based on some quick googling it appears that higher languages are not affected while lower ones are. For example, there's a bunch of SO threads about trying to maintain JSON key order in C++ JSON libraries: https://stackoverflow.com/questions/30837519/writing-in-order-to-jsoncpp-c The JSON spec does not include ordering. It says: "You can assign an order to the properties of an object if you want, but that's your thing, not JSON's thing." I'll go ahead and close the issue anyway because I'm just tired. Thanks. |
I'm sorry it's tiring to you but this really isn't a bug in Node.js but in those frameworks :/ For what it's worth you can:
In either case opening a radar sounds like a reasonable approach. |
What is the bug?
The documentation for conditional exports in a package.json file is here: https://nodejs.org/api/packages.html#packages_conditional_exports
It specifies that the order of keys in the
exports
object matters within a package.json file. However, that is a direct violation of the JSON language specification. By definition properties of JSON objects are unordered: https://www.json.org/json-en.htmlHow do you reproduce the bug?
Consider this package.json file:
If we attempt to use this package via
const thing = require('thing')
, node will throw a huge error because it resolves to thedefault
entry inexports
, which is a*.js
file.This is because node is iterating the entries of
exports
in the order provided. This behavior is incorrect. Instead, node should load the entireexports
object and then follow a standard approach to select the best match from the entries provided. In this case, since we are usingrequire()
, that might be:exports
contain arequire
entry? If yes, use it.exports
contain anode
entry? If yes, use it.exports
contain adefault
entry? If yes, use it.In other words, the ORDER of the entries in package.json should be irrelevant. That should be an internal implementation detail of node, not a user-managed item.
What is the expected behavior?
Given an object for a "conditional export" that contains entries for
require
,import
,node
, anddefault
, the ORDER of those entries in the package.json file MUST be irrelevant. Otherwise, package.json files are not JSON.Why This Matters
The current approach requires a human to manually write the
exports
field. A user cannot, for example, use a JSON library to manipulate a package.json file because JSON libraries (correctly) assume that the order of properties on an object does not matter—they do not provide a way to manually specify that property X must come after property Y.In my case, I was using a JSON library to remove extraneous fields and whitespace from a package.json file and this violation of the JSON spec took a few hours off my life: ai/nanoid#267
The text was updated successfully, but these errors were encountered: