ERR_PACKAGE_PATH_NOT_EXPORTED when file-type used in nest.js
Closed this issue · 9 comments
Hi,
I'm using file-type in nest.js and I've this error, using the last 19.4.1 version.
nest start
node:internal/modules/cjs/loader:597
throw e;
^
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in D:\dev\olhos\nest-test\node_modules\file-type\package.json
at exportsNotFound (node:internal/modules/esm/resolve:304:10)
at packageExportsResolve (node:internal/modules/esm/resolve:594:13)
at resolveExports (node:internal/modules/cjs/loader:590:36)
at Module._findPath (node:internal/modules/cjs/loader:667:31)
at Module._resolveFilename (node:internal/modules/cjs/loader:1129:27)
at Module._load (node:internal/modules/cjs/loader:984:27)
at Module.require (node:internal/modules/cjs/loader:1231:19)
at require (node:internal/modules/helpers:179:18)
at Object.<anonymous> (D:\dev\olhos\nest-test\src\app.controller.ts:3:1)
at Module._compile (node:internal/modules/cjs/loader:1369:14) {
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}
To simply reproduce the problem:
npm i -g @nestjs/cli
nest new nest-test
cd nest-test
npm i file-type
Edit the sample controller, scaffolded from nest.js:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { fileTypeFromFile } from 'file-type';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
async getHello(): Promise<string> {
return (await fileTypeFromFile('Unicorn.png')).mime;
}
}
nest start
Any idea?
Thanks
You need to open an issue on Nest.js instead. This package is configured correctly. They probably don't properly support the exports
field in package.json.
Coincidently had the same problem the same day, just a couple hours earlier :D Following up here to share what I've found:
The problem lies on neither side. It's Typescript, which compiles the dynamic import down to a require
, which in turn obviously does not work.
Here are some options to circumvent it: TypeStrong/ts-node#1290
If you choose Option 1 (or Option 2 respectively), you need to declare the complete path to this module, as it (at least to my understanding) does not define a default properly.
@sindresorhus I'm not sure about the implications, so maybe you can evaluate it. If you add default
entries for the exports, you can properly import the module with the given method above, without the need for the complete path. Does that break something else in a different setup, or can you easily add that?
diff --git a/package.json b/package.json
index 08f1ca3..ef6f98f 100644
--- a/package.json
+++ b/package.json
@@ -15,16 +15,19 @@
".": {
"node": {
"types": "./index.d.ts",
- "import": "./index.js"
+ "import": "./index.js",
+ "default": "./index.js"
},
"default": {
"types": "./core.d.ts",
- "import": "./core.js"
+ "import": "./core.js",
+ "default": "./core.js"
}
},
"./core": {
"types": "./core.d.ts",
- "import": "./core.js"
+ "import": "./core.js",
+ "default": "./core.js"
}
},
"sideEffects": false,
Our formal recommendation is to switch your module to ESM.
Maybe redundant to the solutions already provided here, this is a workaround which worked for me:
import * as path from 'path';
/**
* Import 'file-type' ES-Module in CommonJS Node.js module
*/
(async () => {
const { fileTypeFromFile } = await (eval('import("file-type")') as Promise<typeof import('file-type')>);
const type = await fileTypeFromFile(path.join('..', 'fixture', 'fixture.gif'));
console.log(type);
})();
Which is part of full working demo, using file-type
19.4.1, can be found here
Which is fine in theory, but does not work in the context of a nest application, as nest (sadly) explicitly is not switching away from commonjs. And obviously, depending on the size of your project, this might not be feasible or even possible, regardless
Your solution works in a similar fashion as the ones provided in the ts-node discussion, by skipping the transformation of the import to require, with the difference of using eval, which always is a red flag, even if you hardcode it.
I would rather use one of the provided solutions and have file-type
properly define its exports, which it isn't according to the guidelines:
When using environment branches, always include a "default" condition where possible. Providing a "default" condition ensures that any unknown JS environments are able to use this universal implementation, which helps avoid these JS environments from having to pretend to be existing environments in order to support packages with conditional exports. For this reason, using "node" and "default" condition branches is usually preferable to using "node" and "browser" condition branches.
PS: your typing can be simplified
- const { fileTypeFromFile } = await (eval('import("file-type")') as Promise<typeof import('file-type')>);
+ const { fileTypeFromFile } = await eval('import("file-type")') as typeof import('file-type');
I would rather use one of the provided solutions and have file-type properly define its exports, which it isn't according to the guidelines:
When using environment branches, always include a "default" condition where possible. Providing a "default" condition ensures that any unknown JS environments are able to use this universal implementation, which helps avoid these JS environments from having to pretend to be existing environments in order to support packages with conditional exports. For this reason, using "node" and "default" condition branches is usually preferable to using "node" and "browser" condition branches.
That is exactly what we have done (line #20):
Lines 16 to 23 in 988bf4b
The change you propose adding more default
conditions is not at environment branches level.
PS: your typing can be simplified
- const { fileTypeFromFile } = await (eval('import("file-type")') as Promise<typeof import('file-type')>);
- const { fileTypeFromFile } = await eval('import("file-type")') as typeof import('file-type');
✅ 👍🏻
To my understanding this applies transitively, so you need the default in the nesting, as well.
What happens: The resolver checks the exports
, finds .
, finds node
, but does not find a require
or default
, so it goes up the tree and searches for another match, which is default
, but does not find a require
or default
here either, so it says that it can't resolve the module properly. If you add the default
, it knows what to do. (You could add require
instead, as well, but that would implicate that it is a proper commonjs export, which it isn't)
So, in summary: Yes, you are defining a default, but you then limit the default to only import (and types), so it still will get skipped, as it does not match.
With the current `exports` you need to define the complete path to the module, because it falls under the [limitations of tsimportlib](https://github.com/cspotcode/tsimportlib?tab=readme-ov-file#limitations), aka not correctly or too strictly configured modules. By defining the complete path, you skip the resolver, therefore it works.
With the default
added to the node
part of the exports
it does work.
The other additions of default
by me are just for consistency.
If you add a default to the default (but not to node), this is used, but you would have to use the functions from core (to keep it consistent) instead of the node specific ones.
To my understanding this applies transitively, so you need the default in the nesting, as well.
Not to my understanding, in none of the examples provided by the guidelines this is done.
A require
(CommonJS) entry point is not present, hence it should not be resolvable.
It's not done because in all of the examples there is a top level default and in none of them the default is nested itself. Which on the other hand you have done and should therefore provide a default in the default by applying the given rules transitively or strictly adhere to the examples in the guideline and not define a nested default.
It's going down the tree and if it does not find something it goes up and then down again if another entry matches, but in the end it needs to find a path that leads to a file, which does not happen for default, as the only options there are import and type. And with it's options exhausted it fails.
Partially correct. A require is not present, so it should not be resolvable that way explicitly, but you should have a default entrypoint, so
that any unknown JS environments are able to use this universal implementation, which helps avoid these JS environments from having to pretend to be existing environments in order to support packages with conditional exports
And this default entry point will get hit by the commonjs resolver if present
But we are going in circles here. Either you believe me that the default is not reached (and hence noz configured) properly (which is proven by the commonjs resolver failing to hit it) or you don't, I can't force you 🤷