-
-
Notifications
You must be signed in to change notification settings - Fork 184
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
add support for the exports package.json attribute #224
base: 1.x
Are you sure you want to change the base?
Conversation
Thanks so much, and sorry for the long delay :-) Why can't |
If code resolves An alternative approach could be to ignore package exports in an opts.paths result if it doesn't end with |
I was just about to ask for an update over in #222, perfect timing! 😀 |
@bgotink my hope for initial "exports" support (CJS only, to begin with) is that we'd have a top-level option, something like Separately/additionally, we'd need an option for I haven't thought about how that would impact each of the callback function options, but I have a very strong preference to avoid any changes to them if possible. Thoughts? |
This could lead to significantly different behaviour, so putting this behind a flag and flipping the default value in a v2 makes a lot of sense. Currently I've built it with an The reason for this {
"exports": {
"./some-path": {
"require": "./somePath/index.cjs",
"import": "./somePath/index.js",
"types": "./somePath/index.d.ts"
}
}
} Similarly for bundlers supporting a Maybe both the
I'm actually not sure this is necessary. As I've mentioned above, in node the To give an example of what I mean: Assume a dependency called foo with a package.json file {
"exports": {
"./bar": {
"require": "./bar/qux.cjs",
"default": "./bar/qux.js"
},
"./bar/": "./bar/"
},
"type": "module"
} and files
The builtin resolution behaves like so: > path.relative(process.cwd(), require.resolve('foo/bar'))
'node_modules/foo/bar/qux.cjs'
> path.relative(process.cwd(), require.resolve('foo/bar/index'))
'node_modules/foo/bar/index.js'
> path.relative(process.cwd(), require.resolve('foo/bar/qux'))
'node_modules/foo/bar/qux.js'
> path.relative(process.cwd(), require.resolve('foo/bar/qux.cjs'))
'node_modules/foo/bar/qux.cjs'
> process.version
'v14.8.0' The resolution doesn't seem to care about the type of the module, it keeps on resolving to
Should be feasible if we make the loading of exports conditional on the fact that the subpath '/some/deep/path.js' of identifier 'my-module/some/seep/path.js' to resolve isn't changed by the options, otherwise it's impossible to discover from which package.json to read the exports. |
@bgotink ah right, good point. however, for now, i think i'd actually prefer not to allow overriding conditions for now (which, ftr, are distinctly not envs). When ESM support is added, I intend to provide a "module system" switch, and then allow specifying additional conditions, just like node itself. Additionally, for CJS the precedence order is "require, node, default", if i recall correctly. Additionally, CJS extension lookup applies for required things, and type:module affects this, so we need to take that into account. |
I've followed the naming of the pseudocode in the node documentation, where it's called
For node it makes a lot of sense to only support additiional conditions/envs, because these additional values are part of the actual module loading system of node. The idea being to e.g. add For bundlers it would also make sense to only extend the array, but for things like finding the |
That all makes sense. I think, though, that the initial implementation here needs to be somewhat minimal, so we can extend it intentionally to support use cases. There certainly needs to be a way for things to select the "browser" condition, but i'm not sure providing an array of choices is the best way to do that. |
Yeah I can definitely understand that you want to start with minimal support and be very careful about what usecases to support and how to do so. I've made some changes as requested:
|
I've rebased the PR and added some tests for resolving packages with
Ah, now I grasp your meaning! Sorry, I wasn't considering ESM resolution in this PR so I got confused. I'm not sure how you want to tackle ESM support. I can see two approaches: a separate function for ESM resolution or an option to switch between CJS and ESM; or use ESM or CJS depending on the path the identifier is resolved from. |
lib/async.js
Outdated
@@ -75,6 +77,7 @@ module.exports = function resolve(x, options, callback) { | |||
var extensions = opts.extensions || ['.js']; | |||
var basedir = opts.basedir || path.dirname(caller()); | |||
var parent = opts.filename || basedir; | |||
var env = opts.ignoreExportsField === false ? ['require', 'node'] : []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that there should also be an option to specify the env
array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #224 (comment) - I'm intentionally not exposing that just yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be great to find a way to use the test projects here: https://github.com/ljharb/list-exports/tree/main/packages/tests/fixtures, since these cover a large variety of "exports" configurations. Doesn't have to be automated, but it'd be really helpful to even manually validate this PR against them.
I've only partially reviewed this; I'll take a deeper look at it this week.
I've added the fixtures from those tests and ran some tests against them. I could go one step further and download the folder in a postinstall step (or via git submodules?) instead of copying it, if you'd prefer. A couple of remarks though, the tests fail against two fixtures but I think in both cases the problem lies with the fixtures. Failing fixtures
|
@bgotink Regarding |
Any version that supports exports. I just retested with 14.12.0 and the behaviour is as I described.
It's allowed, but it doesn't do what you'd want it to do. The Talking in the terms defined in the pseudocode of the ESM resolution algorithm (link) the imo this makes a lot of sense. Requiring that exported subpaths are configured at the root of the exports object gives packages the freedom of changing the resolved paths of exported subpaths (even setting it to As a sidenote I see the algorithm documentation renamed |
@bgotink hmm, ok - in that case, The rename is great. Yes, it'd be great to make it easy to add As for preact, when I install preact@10.4.1, and use node v13.2 through v13.12, it works fine - but it fails in v13.13+. To be honest this seems like a possible bug in node itself; I'll investigate. |
aha, turns out it was nodejs/node#32351 - which landed in v13.13, after i'd written my list-exports tests, and was intentional, not a bug. node ^12.17, and >= 13.13, will never support that, so it's probably best if list-exports and resolve both ignore the window during which that worked. |
d36eedd
to
b0ac5ec
Compare
I've made some changes:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the list-exports tests, could we use a git submodule instead of copy-pasting things?
Let's change this PR to target 1.x
instead of master - then we can add the "breaking" change of switching the default of ignoreExportsField
to false
in a separate commit.
Any update on this? |
@ljharb & @bgotink I'm wondering if there's a path for someone else to pick up this work and help get it over the line? Would you be able to enumerate the steps that are in your head that need to be done to get it done? It's not clear to me from reading through this. |
@rvagg I'm sure there is such a path. Primarily, I'm trying to update https://github.com/ljharb/list-exports, so that its test fixtures can serve as a proofing mechanism for |
This comment has been minimized.
This comment has been minimized.
Browserify [does not support](browserify/resolve#224) `"exports"` in a `package.json`, and nor can you use `"browser"` in `package.json` [as a hint to Browserify to look in a certain place for a file](browserify/resolve#250 (comment)). This means the output of `ipjs` is not compatible with Browserify since it expects the runtime/bundler to use the `"exports"` field to look up files paths. If Browserify finds a file path, it will then use the `"browser"` values to apply any overrides, which `ipjs` uses to direct it to the `/cjs` folder. The problem is if it can't find the file in the first place it won't use the `"browser"` map to get the overrides. Handily we're generating the `"browser"` field values from the `"exports"` values so we know we have the complete set of files that the user wants to expose to the outside world, and the paths we want people to use to access them. The change in this PR is to use the `"browser"` field values to [mimc the `"exports"` structure in a CJS-compatible directory structure](browserify/resolve#250 (comment)) as per @ljharb's suggestion. For any file that we are overriding with `"browser"` values, we create an empty file (where a resolvable file does not already exist) a the path Browserify expects it to be at, then it'll dutifully use the `"browser"` field to pull the actual file in.
Browserify [does not support](browserify/resolve#224) `"exports"` in a `package.json`, and nor can you use `"browser"` in `package.json` [as a hint to Browserify to look in a certain place for a file](browserify/resolve#250 (comment)). This means the output of `ipjs` is not compatible with Browserify since it expects the runtime/bundler to use the `"exports"` field to look up files paths. If Browserify finds a file path, it will then use the `"browser"` values to apply any overrides, which `ipjs` uses to direct it to the `/cjs` folder. The problem is if it can't find the file in the first place it won't use the `"browser"` map to get the overrides. Handily we're generating the `"browser"` field values from the `"exports"` values so we know we have the complete set of files that the user wants to expose to the outside world, and the paths we want people to use to access them. The change in this PR is to use the `"browser"` field values to [mimc the `"exports"` structure in a CJS-compatible directory structure](browserify/resolve#250 (comment)) as per @ljharb's suggestion. For any file that we are overriding with `"browser"` values, we create an empty file (where a resolvable file does not already exist) a the path Browserify expects it to be at, then it'll dutifully use the `"browser"` field to pull the actual file in.
Perhaps we could have confidence it's correct based on all the test cases added here? Quite a few implementations of |
I still think nodejs/node#44535 would be best. The algorithm/spec comes from node, would be nice if they published the implementation separately so others wouldn't have to reimplement it, at least in JS |
@benmccann it needs to be correct for all supported node versions, which includes a lot of variations. @SimenB while that would be nice, it's highly unlikely |
@ljharb Can you expand on the nature of the support requirements? Which node versions must be supported, and what is the matrix of variations that would need to be tested? |
Ah I found some documentation on it in https://github.com/ljharb/list-exports/tree/main/packages/list-exports |
Would the intention be for this package to detect the version of Node being used and then provide an implementation of the algorithm corresponding to the version of Node that's being used? Perhaps there's some simplifications that could be made to unblock progress. E.g. this package could support |
@conartist6 down to node 0.4, generally; https://www.npmjs.com/package/node-exports-info represents the 8 (as of a year ago; i think it's probably 9 or 10 now) categories i test for. @benmccann yes, the intention is to allow the user to select a specific category. For list-exports, you'd be able to read by default the
That is a viable approach; the problem is that the algorithm needs to be pretty flexible to deal with all of the categories, and i'd be uneasy with shipping a minimal implementation now, that needed to be massively rewritten in a future non-semver-major release. |
As mentioned in yarnpkg/berry#1359 (comment) we (yarn 2) are looking to contribute
exports
support intoresolve
. The idea is to replace parts of our own node resolution implementation inside the PnP resolver with theresolve
package.It was hard for me to grapple what approach worked best or which technical hurdles would present themselves without actually trying to put together a quick implementation.
An anticipated hurdle:
Does this constitute a breaking change?
Assuming the answer is yes, I have currently hidden package exports behind the
env
option, which needs to be passed in to enable exports. If not passed, the package exports are ignored.Some unanticipated hurdles that I ran into:
opts.paths
gets the full request is a problem. For package exportsresolve
needs to read thepackage.json
inside the package.I've changed the parameter that gets passed into
opts.paths
to the package name instead of the full request. This is the easiest solution, but I would consider it a breaking change.The only alternative I can think of is to keep using the full request, but strip the last part of the resulting paths (e.g. if I want to resolve
pkg/deep/path
, strip/deep/path
from the result ofopts.paths
). My biggest problem with that approach is that it'll break ifopts.paths
returns a path that doesn't end with/deep/path
.opts.packageIterator
. I haven't changed the parameter that gets passed in here yet, but it should be kept in sync withopts.paths
.