nodejs / node

Node.js JavaScript runtime ✨🐢🚀✨
https://nodejs.org
Other
107.74k stars 29.67k forks source link

Special treatment for package.json resolution and exports? #33460

Closed ctavan closed 2 years ago

ctavan commented 4 years ago

📗 API Reference Docs Problem

Location

Section of the site where the content exists

Affected URL(s):

Problem description

Concise explanation of what you found to be problematic

With the introduction of pkg.exports a module only exports the paths explicitly listed in pkg.exports, any other path can no longer be required. Let's have a look at an example:

Node 12.16.3:

> require.resolve('uuid/dist/v1.js');
'/example-project/node_modules/uuid/dist/v1.js'

Node 14.2.0:

> require.resolve('uuid/dist/v1.js');
Uncaught:
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './dist/v1.js' is not defined by "exports" in /example-project/node_modules/uuid/package.json

So far, so good. The docs describe this behavior (although not super prominently):

Now only the defined subpath in "exports" can be imported by a consumer: … While other subpaths will error: …

While this meets the expectations set out by the docs I stumbled upon package.json no longer being exported:

> require.resolve('uuid/package.json');
Uncaught:
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './package.json' is not defined by "exports" in /example-project/node_modules/uuid/package.json

For whatever reason I wasn't assuming the documented rules to apply to package.json itself since I considered it package metadata, not package entrypoints whose visibility a package author would be able to control.

This new behavior creates a couple of issues with tools/bundlers that rely on meta information from package.json.

Examples where this issue already surfaced:

Now the question is how to move forward with this?

  1. One option would be to keep the current behavior and improve the documentation to explicitly warn about the fact, that package.json can no longer be resolved unless added to exports. EDIT: Already done in https://github.com/nodejs/node/commit/1ffd182264dcf02e010aae3dc88406c2db9efcfb / Node.js v14.3.0
  2. Another option would be to consider adding an exception for package.json and always export it.

I had some discussion on slack with @ljharb and @wesleytodd but we didn't come to an ultimate conclusion yet 🤷‍♂️ .


boneskull commented 5 months ago

I'm not sure what the use-case is (if it exists) for restricting access to package.json via Node.js' module importing facilities. But the ship has sailed.

That said, to avoid breaking things, it would be helpful for tooling authors if Node.js would expose an API like:

// node:util

getPackageJson(resolvable: string | URL): Promise<unknown>;

getPackageJsonSync(resolvable: string | URL): unknown;

...which would fulfill with the contents of the "closest" (sibling or ancestor) package.json to resolvable (for a rough userland equivalent, see find-pkg-up).

A simpler and easier-to-implement API would expect a resolvable package name or path/URL to package directory, and resolve with the contents of its package.json--without otherwise crawling the directory structure (rough userland equivalent: read-pkg).

Should probably throw/reject if the operation fails.

Right now, adding package.json to exports is just boilerplate that library authors need to remember to add. Which is crap, of course. 😄

cc @GeoffreyBooth @wesleytodd @ljharb

GeoffreyBooth commented 5 months ago

Yes, a new API could work. Up above I suggested getPackageMetadata, as this could be added without any breaking changes to "exports" and has the additional benefit of being able to use the existing internal cache that Node keeps of all the package.json files it’s already read for all modules.

yangmingshan commented 2 months ago

Here's the recommended way to do this with ES modules:

import { readFileSync } from 'fs';
(async () => {
  const pkgPath = await import.meta.resolve('pkg/')
  console.log(pkgPath);
  console.log(readFileSync(new URL('package.json', pkgPath)).toString());
})();

The above also simplifies with TLA of course.

Currently the above only executes with --experimental-import-meta-resolve.

@nodejs/modules-active-members I think we should discuss unflagging this feature.

// Nodejs v22.6.0

import.meta.resolve('@babel/runtime/')
// Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './' is not defined by "exports" in /Users/.../node_modules/@babel/runtime/package.json imported from ...

🥲

boneskull commented 2 months ago

@GeoffreyBooth Should we open a new issue for adding getPackageMeta? Or is there no point unless someone is going to send a PR?