Skip to content

Instantly share code, notes, and snippets.

@guybedford
Last active October 11, 2020 22:29
Show Gist options
  • Save guybedford/4cb418f93ef51f81343d711bbcd80dd5 to your computer and use it in GitHub Desktop.
Save guybedford/4cb418f93ef51f81343d711bbcd80dd5 to your computer and use it in GitHub Desktop.

Exports Patterns Use Cases and Considerations

This gist introduces some examples of the new exports patterns behaviours based on real use cases, as well as a discussion on the matching specificity algorithm considerations.

Example Use Cases

Selective Multiple Exports

Consider a package which exports a folder today:

{
  "exports": {
    ".": "./main.js",
    "./feature": "./feature.js",
    "./src/": "./src/"
  }
}

The individual entries are fine, but the "./src" export is a concern because the src folders can also contain other files like source maps or internal private modules which are then all effectively exposed in exports resolution.

In addition explicit file extensions are always needed.

With exports patterns, we can write this package as:

{
  "exports": {
    ".": "./main.js",
    "./feature": "./feature.js",
    "./src/*": "./src/*.js",
  }
}

which ensures that only .js files in the src folder are exposed improving encapsulation, and also to not need file extensions to be included when importing.

Packages with Conditions

Exports patterns fully compose with conditions and fallbacks in exports in a well-defined way.

Consider a package with ESM and CJS builds with a path export:

{
  "exports": {
    "./features/": {
      "import": "./features/esm/",
      "require": "./features/cjs/
    }
  }
}

Consumers of this package that transpile to CommonJS cannot get proper dual mode support if .mjs or .cjs extensions were in use in the features folder as if they were to import:

import 'pkg/features/x.mjs';

The corresponding extension could not be expected in the cjs folder for the CJS build.

Thus this places an arbitrary restriction on dual mode builds.

With exports patterns, this can be fixed with:

{
  "exports": {
    "./features/*": {
      "import": "./features/*.mjs",
      "require": "./features/*.cjs"
    }
  }
}

In addition, packages like Preact that have naming conventions in their conditional module definitions would be able to significantly shorten their package.json definitions by adopting patterns. See for example https://unpkg.com/[email protected]/package.json which could benefit from the replacement pattern:

{
  "exports": {
    "./*": {
      "browser": "./*/dist/*.module.js",
      "umd": "./*/dist/*.umd.js",
      "require": "./*/dist/*.js",
      "import": "./*/dist/*.mjs"
    },
  }
}

which would replace around 30 lines of unnecessary repetition needed otherwise.

Imports patterns

Exports patterns also compose with package imports in a very well-defined way.

Patterns enable private imports wildcards:

{
  "imports": {
    "#*": "./local/*/index.js"
  }
}

The above allows imports to support arbitrary local imports, without needing to manually provide each one or use file extensions with path imports:

import componentA from '#private-component-a';
import componentB from '#private-component-b';

Similarly patterns can be defined which map to external packages in turn:

{
  "imports": {
    "#preact/*": "preact/*"
  }
}

will map import '#preact/debug' into preact/debug and in turn apply exports configuration from the package. Composition all works out quite naturally while retaining the static determinism of both imports and exports.

Matching Considerations

The pattern matching is restricted to the case of only one wildcard at the end of the left hand side pattern.

This keeps matching specificity highly well defined while catering to the majority of use cases.

It is not uncommon for eg route matching algorithms to restrict to trailing wildcards only.

Alternative: Object-ordering-based matching

Object-ordering based matching was discussed as an alternative approach to matching. In such a scheme, arbitrary wildcard positions could be supported without needing to define their specificity:

{
  "exports": {
    "./x/*-browser": "./dist/browser/x-*.js",
    "./x/*": "./dist/x-*.js",
    "./x/y": "./disty.js"
  }
}

The problem with the above is that the "./x/y" entry will never be matched.

So while this might allow for some more flexible mapping cases, it does introduce possible confusion to users.

Supporting Arbitrary Pattern Positions

If we were to want to extend the proposal to support arbitrary wildcard positions in future, it is possible to define specificity based matching to work as a backwards-compatible addition to the current scheme, and as discussed above, extending specificity matching may be preferable to object order matching provide the most intuitive usability.

For example, such a specificity could be defined based on the shortest wildcard match in the most specific position to the right, which would provide a well-defined integration.

For now though, this is deemed out of scope of the initial implementation. In various discussions around the use cases for arbitrary wildcards, no compelling use case has yet presented itself for arbitrary matching positions.

The current specificity proposal thus provides an intuitive and simple implementation while leaving the door open to extending arbitrary wildcard positions in future should compelling use cases arise for this, but for the initial implementation this is not deemed important given the lack of use cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment