“Exports” Field of Package.json Violates JSON Language Spec

  • Version: 15.11.0
  • Platform: macOS 11.2.1
  • Subsystem: n/a

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.html

How do you reproduce the bug?

Consider this package.json file:

// package.json
{
  "name": "thing",
  "main": "./index.cjs",
  "exports": {
         "default": "./index.js",
         "import": "./index.js",
         "require": "./index.cjs"
  },
  "type": "module"
}

If we attempt to use this package via const thing = require('thing'), node will throw a huge error because it resolves to the default entry in exports, 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 entire exports object and then follow a standard approach to select the best match from the entries provided. In this case, since we are using require(), that might be:

  1. Does exports contain a require entry? If yes, use it.
  2. Does exports contain a node entry? If yes, use it.
  3. Does exports contain a default entry? If yes, use it.
  4. Throw an error.

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, and default, 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

5 thoughts on ““Exports” Field of Package.json Violates JSON Language Spec

  1. Yes, if we check the ECMAScript specification it points to ECMA-404 to define JSON.

    Under “Normative References”:

    ECMA-404, The JSON Data Interchange Format.
    https://ecma-international.org/publications/standards/Ecma-404.htm

    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)

  2. 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.

  3. 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 --conditions would need to be specified if we gave an absolute order to the key traversal. We did at one point look at arrays in nodejs/modules#452 if people want to dig through some history on why the current design was eventually chosen.

  4. 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.

    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 package.json, they could use this form of exports and it just works™.

  5. So if somebody wants to use Swift to format their package.json, they could use this form of exports and it just works™.

    I say we close as resolved?