Skip to content

Add './node' subpath export for node entry point #741

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

Merged
merged 2 commits into from
Feb 25, 2025

Conversation

Borewit
Copy link
Collaborator

@Borewit Borewit commented Feb 21, 2025

Add ./node subpath export

./core has been inherited at the days this package was CJS, where the default exports was Node.js. The core allowed to import the functionality, which did not depend on the Node API. With ES Module implementation, it is the other way around. The non-Node is now the default. Therefor it make more sense expose the Node specific functionality, for those cases where conditional exports cannot be used. Such as the ``"moduleResolution": "bundler"` in TypeScript projects.

I chose to use the default export in this case as generic fallback that always matches, as the subpath import is the user overruling the import in cases where the conditional imports have no, or not the desired effect.

Fixes #652
Fixes #738

Add `module-sync` in exports, exposing entry points to require/ESM loading.
@Borewit Borewit added the API change Major change, dependents may need to update their code label Feb 21, 2025
@Borewit Borewit self-assigned this Feb 21, 2025
@Borewit Borewit requested a review from sindresorhus February 21, 2025 09:44
@sindresorhus
Copy link
Owner

sindresorhus commented Feb 21, 2025

I think the separation of Node.js vs the rest has caused us way too many problems. How about we always export fromFile, but throw inside the function if the fs API is not available. We can use await import inside the function.

That will probably fix all the other import issues too, since our package.json could go back to:

"exports": {
	"types": "./index.d.ts",
	"default": "./index.js"
}

@Borewit
Copy link
Collaborator Author

Borewit commented Feb 21, 2025

#736 (comment)

I think the separation of Node.js vs the rest has caused us way too many problems. How about we always export fromFile, but throw inside the function if the fs API is not available.

That will also mix the dependency tree, definitely a no go. I think in separation is actually working great, and that feature is a massive advantage of the ECMAScript module. Without that, I don't see the point we moved away from CommonJS.
Only in exceptional cases, this does not work, such as the ``"moduleResolution": "bundler"` in TypeScript case.

Yet there are workaround on the user side possible as well, such as raising the node condition manually:

{
  "compilerOptions": {
    "target": "es2022",
    "moduleResolution": "bundler",
    "customConditions": ["node"]
  }
}

That will probably fix all the other import issues too, since our package.json could go back to:

"exports": {
"types": "./index.d.ts",
"default": "./index.js"
}

Getting back to a more basic exports, I propose the following:

	"exports": {
		"node": {
			"types": "./index.d.ts",
			"default": "./index.js"
		},
		"default": {
			"types": "./core.d.ts",
			"default": "./core.js"
		}
	},
  • Drops the subpath exports.
  • Uses default as fallback, shall work for require-esm
  • Preserves the Node.js API with default, seperation

Yet I have nothing against the subpath exports, if that helps some users, it does not disturb anything else.
And that is basically what this PR does.

The biggest issue we have now are import conditions, without any fallback to default or module-sync.

@sindresorhus
Copy link
Owner

That will also mix the dependency tree

The same dependencies are installed for all export types, so not really sure what you mean.

@Borewit
Copy link
Collaborator Author

Borewit commented Feb 21, 2025

That will also mix the dependency tree

The same dependencies are installed for all export types, so not really sure what you mean.

For example node:fs is only exported via the Node.js specific entry point. That Node specific API dependency would end-up in the (browser) default entry point, following your proposal.

@sindresorhus
Copy link
Owner

The method would, yes, but we would simply make it throw when called and the fs api is not available. And as I already commented, we would use dynamic import inside the function, so it should have no effect on non-Node.js consumers, other than a few LOCs of unused code.

@Borewit
Copy link
Collaborator Author

Borewit commented Feb 21, 2025

The method would, yes, but we would simply make it throw when called and the fs api is not available. And as I already commented, we would use dynamic import inside the function, so it should have no effect on non-Node.js consumers, other than a few LOCs of unused code.

To me it feels going from one extreme to the other.

Again, I don't see why we should throw a good working mechanism overboard. Only in the TypeScript bundler the Node condition is not picked up, which probably relies in more in the nature of the bundler, or maybe even on how the bundler's project is setup. I see no reason to drastically change our architecture, but more then happy to add an extra sub-path export if that helps them to plugin the Node functionality.

What is much more important, is to support require-esm, there is massive need for that solution.

I propose to give this PR a try, and see how that works out. I have no problem with sustaining the /core subpath neither, to avoid any breaking, although I cannot imagine it still serves a useful purpose, but maybe I overlooked something.

@sindresorhus
Copy link
Owner

To me it feels going from one extreme to the other.

I don't really see what's extreme about it. There is really nothing inherently tied to Node.js for this package other than the file access, which can easily be isolated to a single method. Regardless of the sub-export problems, having the package as platform agnostic as possible is a good goal.

@Borewit
Copy link
Collaborator Author

Borewit commented Feb 24, 2025

Regardless of the sub-export problems, having the package as platform agnostic as possible is a good goal.

I totally agree with that!

I don't really see what's extreme about it.

The contradiction I see is objecting to my proposal of extending the current import condition with module-sync while then proposing to use default instead—which effectively matches any condition. The current import condition limits us to fewer use cases (for instance, require-esm loading works but is now effectively prohibited), whereas default would open the door to more import scenarios, including options we do not support. Supporting require-esm loading significantly improves accessibility for many users of this ESM module, so I believe we should take advantage of that opportunity. This approach addresses a major weak point in the CommonJS-to-ESM transition.

The second contradiction is moving away from a purist Node.js ESM specification. Your proposal replaces the clear-cut ESM conditions for accessing the Node API with a mechanism that relies on the success of lazy loading the Node.js API.

The third contradiction is the rejection of workarounds. Exporting a sub-path is a very minor change that doesn’t impact anything else, yet it’s dismissed solely because it’s seen as a workaround. In contrast, replacing the Node ESM condition with lazy loading is a drastic workaround with potential side effects.

@sindresorhus
Copy link
Owner

The second contradiction is moving away from a purist Node.js ESM specification.

Sub-exports is a Node.js invented thing and not part of ESM.

Your proposal replaces the clear-cut ESM conditions for accessing the Node API with a mechanism that relies on the success of lazy loading the Node.js API.

No, both are completely valid methods to achieve the same goal.

Exporting a sub-path is a very minor change that doesn’t impact anything else

I may seem minor, but it too can have side-effects. What if a package depends on it, and then a consumer of that package wants to use that package in the browser. Then that package has locked into using the Node.js API since they forced the node sub-export. Enabling users to shoot them self in the foot too easily is not good either.

In contrast, replacing the Node ESM condition with lazy loading is a drastic workaround with potential side effects.

This change too has potential side-effects.

@sindresorhus
Copy link
Owner

I don't really want to waste more time discussing this, so fine, we can do it your way.

`./core` has been inherited at the days this package was CJS, where the default exports was Node.js. The core allowed to import the functionality, which did not depend on the Node API. With ES Module implementation, it is the other way around. The non-Node is now the default. Therefor it make more sense expose the Node specific functionality, for those cases where conditional exports cannot be used. Such as the ``"moduleResolution": "bundler"` in TypeScript projects.
@Borewit Borewit force-pushed the replace-core-with-explicit-node-path-export branch from acaa556 to 4ef2866 Compare February 24, 2025 18:14
@Borewit Borewit changed the title Replace core with explicit node path export Add './node' subpath export for node entry point Feb 24, 2025
@Borewit Borewit removed the API change Major change, dependents may need to update their code label Feb 24, 2025
@Borewit Borewit requested a review from sindresorhus February 24, 2025 18:16
@sindresorhus sindresorhus merged commit 8d39f66 into main Feb 25, 2025
6 checks passed
@sindresorhus sindresorhus deleted the replace-core-with-explicit-node-path-export branch February 25, 2025 06:52
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.

fileTypeFromFile not found Can't access node functions when tsconfig moduleResolution is set to "bundler"
2 participants