sindresorhus / file-type

Detect the file type of a file, stream, or data
MIT License
3.72k stars 354 forks source link

ERR_PACKAGE_PATH_NOT_EXPORTED when file-type used in nest.js #661

Closed robertok-weiji closed 3 months ago

robertok-weiji commented 3 months ago

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

sindresorhus commented 3 months ago

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.

RobertBeilich commented 2 months ago

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: https://github.com/TypeStrong/ts-node/discussions/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,
Borewit commented 2 months ago

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

RobertBeilich commented 2 months ago

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');
Borewit commented 2 months ago

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):

https://github.com/sindresorhus/file-type/blob/988bf4bc9f9bc98e8f3360da4dfa36e5caa455b3/package.json#L16-L23

The change you propose adding more default conditions is not at environment branches level.

Borewit commented 2 months ago

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');

✅ 👍🏻

RobertBeilich commented 2 months ago

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, 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.

Borewit commented 2 months ago

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.

RobertBeilich commented 2 months ago

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 🤷