Skip to content
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 exports field to package.json #208

Merged
merged 5 commits into from
Jul 14, 2023
Merged

Add exports field to package.json #208

merged 5 commits into from
Jul 14, 2023

Conversation

Mrtenz
Copy link
Member

@Mrtenz Mrtenz commented Jul 12, 2023

Following discussion in #203, I removed the exports field from the package.json because it seemed unnecessary. Unfortunately, not having it breaks when importing a package when Node.js is running in ESM mode, because Node.js doesn't look at the module field at all. This causes issues like in MetaMask/key-tree#144.

This adds a two exports fields:

  • .: The main export, which is used when importing a package from the root. This means that we cannot reliably import from subpaths anymore.
  • ./package.json: To allow resolving the package.json path (and importing it, if desired), we also export this.

Since Browserify does not support the exports field, we can't use ./* exports (i.e., package/foo resolves to package/dist/esm/foo or package/dist/cjs/foo depending on the environment), as it would break in the extension.

@Mrtenz Mrtenz requested a review from a team as a code owner July 12, 2023 15:42
package.json Outdated
"require": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts"
},
"./dist/cjs/*": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so I understand this correctly from your comment here, because Browserify ignores exports, the paths on the left-hand side of exports (aside from ".") need to be written so that they resolve to real files in the filesystem. We do this so that if the extension uses a library that contains a subpath import e.g., @metamask/key-tree/dist/cjs/some/path, will continue to work.

However, this seems to imply two things: 1) the responsibility for resolving an import path shifts from the consumer to the library itself, and 2) we cannot create fully ESM-compatible libraries.

Say we publish a library that contains a subpath import of another library. Ideally we could use a shortcut for that import (e.g. @metamask/key-tree/some/path) and it would resolve to either the CJS or ESM version depending on what the consumer is using (a bundler or straight Node). But we can't do that, so we have to be explicit and use the CJS version (@metamask/key-tree/dist/cjs/some/path).

But if we do that, wouldn't this mean that in the compiled version of our library, both the CJS and ESM builds will contain this import? So even if in our ESM build, we'd potentially have a mixture of ESM and CJS?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give an example of "a subpath import of another library"? I'm not sure what that means, or why we'd want to support it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if we do that, wouldn't this mean that in the compiled version of our library, both the CJS and ESM builds will contain this import? So even if in our ESM build, we'd potentially have a mixture of ESM and CJS?

Yeah, that's the downside of doing this. I thought about doing something like this:

{
  "./dist/cjs/*": {
     "import": "./dist/esm/*.js",
     "require": "./dist/cjs/*.js"
  }
}

So even if you import ./dist/cjs/foo, and your bundler supports exports, it will resolve to ./dist/esm/foo. But this feels very hacky.

I wonder if there's a way to support exports in Browserify through a plugin, but I couldn't find anything about this. That would solve all problems I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give an example of "a subpath import of another library"? I'm not sure what that means, or why we'd want to support it.

For example:

  • utils imports abi-utils/dist/cjs/foo
  • snaps-utils imports utils.
  • snaps-utils is now indirectly using abi-utils/dist/cjs/foo, even though the bundler may support ESM.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, my brain somehow read "subpath import" as "subpath export". That makes more sense.

@Gudahtt
Copy link
Member

Gudahtt commented Jul 12, 2023

Perhaps we could omit the dist/* exports, and commit ourselves to not relying on file imports anymore? We can be more diligent about updating the package exports for our libraries. That's something we've been trying to do anyway, this can serve as a good excuse.

@Mrtenz
Copy link
Member Author

Mrtenz commented Jul 12, 2023

Perhaps we could omit the dist/* exports, and commit ourselves to not relying on file imports anymore? We can be more diligent about updating the package exports for our libraries. That's something we've been trying to do anyway, this can serve as a good excuse.

I'm not sure how much work would be involved in this, but if it's not too much, that seems like a good option too. A regex search in the Snaps repo (import .* from '@metamask/.*/.*') shows there's a couple of cases where we do this, which can be easily addressed.

"require": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts"
},
"./package.json": "./package.json"
Copy link
Member Author

@Mrtenz Mrtenz Jul 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is useful for resolving the root of the package. I'm not sure if it should be added to the module template, but it doesn't hurt I suppose?

We use this in the Snaps repo several times, which doesn't work without this line:

path.dirname(require.resolve('package/package.json'));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I guess it doesn't hurt 🤷

@@ -24,7 +32,8 @@
"build:cjs": "swc src --out-dir dist/cjs --config-file .swcrc.build.json --config module.type=commonjs",
"build:clean": "rimraf dist && yarn build",
"build:docs": "typedoc",
"build:esm": "swc src --out-dir dist/esm --config-file .swcrc.build.json --config module.type=es6",
"build:esm": "swc src --out-dir dist/esm --config-file .swcrc.build.json --config module.type=es6 && yarn build:esm:package",
"build:esm:package": "echo >dist/esm/package.json \"{\\\"type\\\":\\\"module\\\"}\"",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently Node.js doesn't allow using .js for both the CJS and ESM version in the same project (??), but you can work around it by adding a package.json to the ESM folder.

@Mrtenz Mrtenz requested review from mcmire and Gudahtt July 12, 2023 17:44
@mcmire
Copy link
Contributor

mcmire commented Jul 13, 2023

Perhaps we could omit the dist/* exports, and commit ourselves to not relying on file imports anymore? We can be more diligent about updating the package exports for our libraries. That's something we've been trying to do anyway, this can serve as a good excuse.

I like this. Maybe it shouldn't be a blocker for this PR? But I do think it's a good idea.

Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants