Repository navigation
export spec clarification with require xor import and non-JS cases #41686
Description
Activity
- addeddocIssues and PRs related to Node.js documentation.Issues and PRs related to Node.js documentation.
on Jan 25, 2022 @nodejs/modules
A package name on the command line is “whatever the package’s package.json says the format of the entry point is”, which is deterministic from the file extension of the main/exports dot, and optional “type” field.
There wouldn’t be any standard behavior for module types that don’t exist, like css or html.
Ok, that makes sense. I'll do that. Thanks!
If esbuild supported importing things from HTML, which it may one day, there would also be those cases to consider.
Technically from HTML there's a valid 3rd option ("script"?) in addition to
requireandimport. But I'm not sure how valuable supporting that would be in a relatively new bundler. For<script type="module">, it seems fairly unambiguouslyimport.Right now esbuild handles these non-JS cases by still running the exports logic but just without the import or require conditions active, since it's neither an import nor a require.
That feels sensible, partially also because I'm not sure what the semantics are of "including JS from CSS" (does it fail if the CSS isn't imported in turn, transitively, by some other JS..?). Effectively, this seems like something that might have to be up to tools because these non-JS edges may have wildly different semantics across tools (e.g. import-CSS as a side effect vs. as importing a stylesheet object). My guess is that just considering those to be
importedges is the most useful choice because that appears to be the authoring trend for reusable front-end code.But my cop-out answer would be "node isn't in the best position to make a recommendation because it lacks domain expertise". I would expect this question to require a chat between parcel/esbuild/webpack/rollup... because in that group there's a higher chance of an informed outcome. :)
Node.js itself does not support package resolution for the entry point so does not hit this problem. Node.js mains can only be direct file system paths, and then Node.js uses the standard format algorithms (as mentioned by @ljharb) to determine the initial module format.
So the entry-as-a-package problem is unique to build tools. In my own tooling I make
"import"the first condition by default so that ES modules are preferred to CommonJS. If that leads to the dual module issues, then perhaps having a specific configuration to override this default would be useful.Thanks so much for your replies.
I'm not sure what the semantics are of "including JS from CSS"
I'm not sure either. This just isn't allowed in esbuild (it's a build error). I think this case would be more for other non-JS assets that end up using the
exportsmap too. People publish all kinds of things to npm. JS assets do come up in the entry point and HTML cases though.A package name on the command line is “whatever the package’s package.json says the format of the entry point is”, which is deterministic from the file extension of the main/exports dot, and optional “type” field.
I don't think the file extension applies here because the command line isn't a file, and doesn't have an extension. This corresponds to someone doing
esbuild --bundle pkg-name. And you can't use the file extension of the default file for the package because you don't have that information yet; you need to figure out what condition to apply to learn what the default file for the package is.What I took this to mean was to inspect the
"type"field in thepackage.jsonfile and use therequirecondition unless"type": "module"is present, in which case theimportcondition is used instead. That makes sense to me because this is an existing behavior and then the behavior in this edge case is determined by the package author's decisions, so it's sort of under their control what happens.In my own tooling I make
"import"the first condition by default so that ES modules are preferred to CommonJS. If that leads to the dual module issues, then perhaps having a specific configuration to override this default would be useful.I could also see "
importshould be the default" or "it's up to the build tool" for the entry point case because there are arguments for either. The person who filed the original issue with me wanted the selection to be determined by the bundler's configured output format, which is also reasonable. I mainly wanted to check here first to see if there was a definite answer to what I should do (especially if this has come up before) so I don't end up violating the specification. But it's ok if the answer is that it's left unspecified too.@evanw type module changes what .js means but it doesn’t force import; i can still require a package with type module if it’s main/dot points to a .cjs file (and probably also a .json file)
i can still require a package with type module if it’s main/dot points to a .cjs file
I believe you're talking about the extension of
"."inside of"exports". However, what we are trying to figure out is whether to apply theimportorrequirecondition so we can evaluate"."inside of"exports"in the first place. So what you're suggesting is circular because you'd need the information to compute itself, so that information isn't available (if I understand you correctly). For example, consideresbuild --bundle pkg-foowhere the packagepkg-foohas apackage.jsonfile that looks like this:{ "name": "pkg-foo", "exports": { ".": { "import": "./foo.mjs", "require": "./foo.cjs" } } }We are trying to figure out what conditions to apply so we can figure out what file to use as the entry point. We don't know whether to apply
importorrequireat this point because the provided package pathpkg-foocame from the command line and not from an import statement or a require call. We can't use the extension of the entry point file because there isn't just one, and there's nodefaultcondition so there's no canonical one.Maybe you're saying esbuild should just not apply the
"exports"conditions at all in this case and instead fall back to the legacymainfield lookup instead?this seems to have some overlap with determining which loader to initiate for the entry point as in #41552
@evanw i see what you mean. since node's default entry point type is CJS, and will remain so for the foreseeable future, that's what i'd suggest assuming.
Packages that aren't concerned with backwards compatibility (imo, user-hostile ones) omit "main" entirely, so a fallback to "main" isn't something you can rely on.
@ljharb the entry point isn't CJS there is a function that determines which loader is used
@bmeck Good to know! If it's a package name then, how does that function determine it?
@ljharb CLI args are always converted to a file path (either as url or path string depending) so you cannot ref a package name.
gotcha. and what if i do
node node_modules/foo?@ljharb that is actually complex... it first does a CJS resolve against the full file path (normally , not always) and then determines if it should do an ESM or CJS load operation which can be a bit thrashy
Sounds like "CJS is the default" would apply, then, altho with an ESM fallback.
@ljharb that isn't what happens though
Reacted by Jordan Harbandnode/lib/internal/modules/run_main.js
Lines 30 to 45 in 775bfd1
function shouldUseESMLoader(mainPath) { const userLoader = getOptionValue('--experimental-loader'); if (userLoader) return true; const esModuleSpecifierResolution = getOptionValue('--experimental-specifier-resolution'); if (esModuleSpecifierResolution === 'node') return true; // Determine the module format of the main if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) return true; if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) return false; const pkg = readPackageScope(mainPath); return pkg && pkg.data.type === 'module'; } I just ran a test of the other bundlers: https://github.com/evanw/entry-point-resolve-test. Webpack and Rollup are consistent with each other, and Parcel just consistently fails to handle this case. It seems like the rules should be as follows:
- If
exportsexists, try resolving withimport, and fail if nothing matches - Otherwise, try
module - Otherwise, try
main
Since the other bundlers are consistent and it sounds like perhaps there isn't strong consensus here, I think I should probably just do what they do.
- If
- If
exportsexists, try resolving withimport, and fail if nothing matches
I agree with this approach. I think for most cases, if the author has included
"exports"and"import"it’s a pretty modern package and they probably expect the ESM version to be used first with the CommonJS version as the fallback. I don’t have numbers but I’d bet that ESM with CommonJS fallback is a far more common pattern than CommonJS with ESM fallback.Where does
"exports""require"fit into the algorithm? Between steps 2 and 3? As part of step 1, after tryingimportanddefault?- If
Couldn’t the ordering of the conditions matter? That way, whatever they put first is the priority.
I went with
importsemantics for this in esbuild by the way, as described above. So I don't need this issue open anymore. Feel free to close it if you don't need it either.Reacted by Geoffrey Booth
Affected url(/sitelet?url=https%3A%2F%2Fgithub.com%2Fnodejs%2Fnode%2Fissues%2Fs)
https://nodejs.org/api/packages.html#conditional-exports
Description of the problem
I'm coming here from evanw/esbuild#1956. Specifically the esbuild bundler implements node's spec for
exportsinpackage.jsonfiles. Node's documentation forexportssays this:This is what esbuild implements. The case I'm looking for clarification on is that there are packages that just have
requireandimportbut notdefault. This is reasonable as the example in the documentation also doesn't include adefaultcondition, and the documentation saysimportandrequireare mutually exclusive (i.e. exactly one should be present).However, esbuild performs the following kinds of path resolution:
import-statementrequire-callentry-pointimport-ruleurl-tokenI understand what should happen with the first two but not with the last three. An entry point is path resolution that happens due to a package name provided on the command line. There is no import or require in that case. And the last two happen when you import things from CSS (something a bundler has to deal with) where there is also no JS
importstatement orrequirecall. If esbuild supported importing things from HTML, which it may one day, there would also be those cases to consider.Right now esbuild handles these non-JS cases by still running the
exportslogic but just without theimportorrequireconditions active, since it's neither an import nor a require. But then that breaks when there's nodefaultcondition. Do you have any opinion for what should happen here, as the authors of this specification? Some options:importorrequireand fail path resolution if nothing applies (esbuild's current behavior)importorrequireand fall back to legacymodule/mainfields if nothing appliesimportorrequireto apply arbitrarilyimportandrequireexportsrules at all and only use legacymodule/mainfields