Repository: prettier/prettier (JavaScript)
Date: 2023-08-11 09:40:41+00:00
Task type: Bug Fix

Issue:
Prettier 3.x - Package subpath is not defined
Hi, after updating prettier to `3.0.0`, I am trying to set the main configuration from the shared configuration (npm package) but I'm getting errors via terminal:

```
["INFO" - 3:55:52 PM] Formatting file...
["ERROR" - 3:55:52 PM] Error resolving prettier configuration for ...
["ERROR" - 3:55:52 PM] Package subpath './prettier-base' is not defined by "exports" in .../node_modules/pkg/package.json

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './prettier-base' is not defined by "exports" in .../node_modules/pkg/package.json
```

### Current pkg setup

**node_modules/pkg/dist/prettier/prettier-base.mjs**

```js
export default {
  // ...
}
```

**node_modules/pkg/package.json**

```json
{
  "type": "module",
  "exports": {
    "./prettier-base": {
      "types": "./dist/prettier/prettier-base.d.ts",
      "import": "./dist/prettier/prettier-base.mjs"
    }
  }
}
```

---

And later I import it into the project:

**package.json**

This throws the errors I mentioned above:

```json
{
  "type": "module",
  "prettier": "pkg/prettier-base"
}
```

This works as expected without errors:

```json
{
  "type": "module",
  "prettier": "./node_modules/pkg/dist/prettier/prettier-base.mjs"
}
```

and this also works as expected:

```js
// prettier.config.js

import baseConfig from 'pkg/prettier-base'

export default baseConfig
```

Any advice?



Issue discussion:
:wave: @ivodolenc, we use the issue tracker exclusively for development purposes. For questions, please use [Stack Overflow](https://stackoverflow.com/questions/ask?tags=prettier).
Hi @kachkaev, may I ask why you closed this? Do I need you to share an example repo?
👋 @ivodolenc! I marked this issue as a question and it got automatically closed, according to this repo’s [config](https://github.com/prettier/prettier/blob/825425c57f00268abaf3964b441a7649c8004ba0/.github/ISSUE_TEMPLATE/config.yml#L5). I believe that your issue is external to Prettier and is a generic one, related to configuring ESM modules.

Either way, it is hard to come up with advice without a full reproduction. You can try seeking for help on StackOverflow or in GitHub Discussions by sharing a repository or a code sandbox. Issues in this repo are dedicated to bug tracking and feature requests.
I wrote "Any advice?" because maybe someone on the team already knows a quick fix or something... This is not a question but a bug report because the manual path from `node_modules` works as expected.

Also, when importing the configuration from the subpath into the custom `prettier.config.js`, that configuration works as expected (I've provided both examples above). Package subpaths works fine on my side.

It is possible that Pretier has problems finding node subpaths when the configuration is extended via package.json (prettier v3.x.x) but if you don't want to investigate further, I'm fine with that.
Looking at your terminal output, looks like this is an issue specific to VS Code. This repo is for tracking issues with Prettier core only. After a bit of digging, I think I found the issue report you are looking for: https://github.com/prettier/prettier-vscode/issues/3066.

If you find a bug in Prettier core, please follow the issue template and make sure that there are reproduction steps that others can easily reproduce.
[Here](https://github.com/ivodolenc/prettier-issue-15195) is a minimal reproduction with a simple example. The npm package is live and you can also see the github repo with all the code.
Thanks for creating the reproduction repo @ivodolenc! Things make a bit more sense now. I was initially confused by patterns like `["INFO" - 3:55:52 PM]` and `["ERROR" - 3:55:52 PM]` in your issue report. They are often a sign of people having troubles with VS Code, which is external to this repo. Configuring ESM is often a bit tricky, so I thought this was a setup question of some sort. Existence of https://github.com/prettier/prettier-vscode/issues/3066 confused me even more. Sorry for that!

Using the [package you have published](https://unpkg.com/browse/prettier-issue-test-config@0.0.1/), I think I have been able to reproduce your steps in a terminal, outside VS Code:

```sh
cd /tmp

mkdir prettier-issue-15195
cd prettier-issue-15195

cat << EOF > package.json
{
  "type": "module",
  "prettier": "prettier-issue-test-config/prettier-base"
}
EOF

npm install --save-exact --save-dev prettier@3.0.0 prettier-issue-test-config@0.0.1

npx prettier --log-level=debug --write package.json
```

Output:

```
[debug] normalized argv: {"":["package.json"],"cache":false,"color":true,"editorconfig":true,"write":true,"logLevel":"debug","configPrecedence":"cli-override","debugRepeat":0,"ignorePath":[".gitignore",".prettierignore"],"plugins":[],"_":["package.json"],"__raw":{"_":["package.json"],"cache":false,"color":true,"editorconfig":true,"write":true,"log-level":"debug","config-precedence":"cli-override","debug-repeat":0,"ignore-path":[".gitignore",".prettierignore"],"plugin":[]}}
[debug] resolve config from '/private/tmp/prettier-issue-15195/package.json'
[error] Invalid configuration for file "/private/tmp/prettier-issue-15195/package.json":
[error] Package subpath './prettier-base' is not defined by "exports" in /private/tmp/prettier-issue-15195/node_modules/prettier-issue-test-config/package.json
```

Interestingly, when I make a somewhat illogical change to `node_modules/prettier-issue-test-config/package.json`, running `npx prettier --log-level=debug --write package.json` ends up working:

```diff
{
  "name": "prettier-issue-test-config",
  "version": "0.0.1",
  "license": "MIT",
  "type": "module",
  "exports": {
    "./prettier-base": {
+     "require": "./dist/prettier/prettier-base.mjs",
      "types": "./dist/prettier/prettier-base.d.ts",
      "import": "./dist/prettier/prettier-base.mjs"
    }
  },
```

```
[debug] normalized argv: {"":["package.json"],"cache":false,"color":true,"editorconfig":true,"write":true,"logLevel":"debug","configPrecedence":"cli-override","debugRepeat":0,"ignorePath":[".gitignore",".prettierignore"],"plugins":[],"_":["package.json"],"__raw":{"_":["package.json"],"cache":false,"color":true,"editorconfig":true,"write":true,"log-level":"debug","config-precedence":"cli-override","debug-repeat":0,"ignore-path":[".gitignore",".prettierignore"],"plugin":[]}}
[debug] resolve config from '/private/tmp/prettier-issue-15195/package.json'
[debug] loaded options `{"semi":false,"tabWidth":2,"singleQuote":true}`
[debug] applied config-precedence (cli-override): {"singleQuote":true,"semi":false,"tabWidth":2}
package.json 21ms
```

I tried removing `"prettier": "prettier-issue-test-config/prettier-base",` from `package.json` and creating `.prettierrc.json` with a reference to your package path:

```json
"prettier-issue-test-config/prettier-base"
```

This also errored without the `require` `exports` hack:

```
[debug] normalized argv: {"":["package.json"],"cache":false,"color":true,"editorconfig":true,"write":true,"logLevel":"debug","configPrecedence":"cli-override","debugRepeat":0,"ignorePath":[".gitignore",".prettierignore"],"plugins":[],"_":["package.json"],"__raw":{"_":["package.json"],"cache":false,"color":true,"editorconfig":true,"write":true,"log-level":"debug","config-precedence":"cli-override","debug-repeat":0,"ignore-path":[".gitignore",".prettierignore"],"plugin":[]}}
[debug] resolve config from '/private/tmp/prettier-issue-15195/package.json'
[error] Invalid configuration for file "/private/tmp/prettier-issue-15195/package.json":
[error] Package subpath './prettier-base' is not defined by "exports" in /private/tmp/prettier-issue-15195/node_modules/prettier-issue-test-config/package.json
```

Finally, I replaced `.prettierrc.json` with `prettier.config.js`, similarly to what you did:

```js
export { default } from "prettier-issue-test-config/prettier-base";
```

This worked:

```
[debug] normalized argv: {"":["package.json"],"cache":false,"color":true,"editorconfig":true,"write":true,"logLevel":"debug","configPrecedence":"cli-override","debugRepeat":0,"ignorePath":[".gitignore",".prettierignore"],"plugins":[],"_":["package.json"],"__raw":{"_":["package.json"],"cache":false,"color":true,"editorconfig":true,"write":true,"log-level":"debug","config-precedence":"cli-override","debug-repeat":0,"ignore-path":[".gitignore",".prettierignore"],"plugin":[]}}
[debug] resolve config from '/private/tmp/prettier-issue-15195/package.json'
[debug] loaded options `{"semi":false,"tabWidth":2,"singleQuote":true}`
[debug] applied config-precedence (cli-override): {"singleQuote":true,"semi":false,"tabWidth":2}
package.json 21ms
```

ESM support for config files was added in https://github.com/prettier/prettier/pull/13130. There are lots of test cases, but maybe we’ve found a gap? @fisker, WDYT?

@ivodolenc, thanks again for being persistent with this report, and apologies again for closing the issue too early!
 
@kachkaev  np 👍

I'm working on shared configs and couldn't get it to work so I assumed something might be wrong since the manual configuration works fine.

Maybe I should have shared an example repo right away so it could be easier to understand.
Looks like a bug. We should not try "resolve" here 
https://github.com/prettier/prettier/blob/825425c57f00268abaf3964b441a7649c8004ba0/src/config/load-external-config.js#L23

https://github.com/prettier/prettier/blob/825425c57f00268abaf3964b441a7649c8004ba0/src/utils/import-from-file.js#L6
~Maybe add `"./*": "./*"` to `exports` field to your `package.json` will work?~ Tested, won't work.
Add `error?.code !== "ERR_PACKAGE_PATH_NOT_EXPORTED"` to

https://github.com/prettier/prettier/blob/825425c57f00268abaf3964b441a7649c8004ba0/src/config/load-external-config.js#L17

Seems fix it.

Maybe I was wrong about https://github.com/prettier/prettier/issues/15195#issuecomment-1662090081, we can still import config by URL, but not let `require()` call to throw,

I don't have time to add test for it, hope someone can send a PR with this fix.
Also found a workaround.

```js
// dist/prettier/prettier-base.cjs
module.exports = import('./prettier-base.mjs').then(m => m.default)
```

```diff
  "exports": {
    "./prettier-base": {
+      "require": "./dist/prettier/prettier-base.cjs"
    }
  },
```
> Also found a workaround.

I can confirm that this workaround works. I just added the [.cjs](https://github.com/ivodolenc/prettier-issue-15195/blob/main/dist/prettier/prettier-base.cjs) file to the pkg and after updating the pkg and restarting VSCode this works (config is extended via package.json). 

Updates can be followed [here](https://github.com/ivodolenc/prettier-issue-15195/commits/main).

Solution patch:
diff --git a/changelog_unreleased/api/15233.md b/changelog_unreleased/api/15233.md
new file mode 100644
index 000000000000..cd64b782f0d9
--- /dev/null
+++ b/changelog_unreleased/api/15233.md
@@ -0,0 +1,9 @@
+#### Support shared config that forbids `require()` (#15233 by @fisker)
+
+If an external shared config package is used, and the package `exports` don't have `require` or `default` export.
+
+In Prettier stable Prettier fails when attempt to `require()` the package, and throws an error.
+
+```text
+Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in <packageName>/package.json
+```
diff --git a/src/config/load-external-config.js b/src/config/load-external-config.js
index 3cba4fabbb66..96988a08c859 100644
--- a/src/config/load-external-config.js
+++ b/src/config/load-external-config.js
@@ -1,6 +1,11 @@
 import requireFromFile from "../utils/require-from-file.js";
 import importFromFile from "../utils/import-from-file.js";
 
+const requireErrorCodesShouldBeIgnored = new Set([
+  "MODULE_NOT_FOUND",
+  "ERR_REQUIRE_ESM",
+  "ERR_PACKAGE_PATH_NOT_EXPORTED",
+]);
 async function loadExternalConfig(config, filepath) {
   /*
   Try `require()` first, this is how it works in Prettier v2.
@@ -12,10 +17,7 @@ async function loadExternalConfig(config, filepath) {
   try {
     return requireFromFile(config, filepath);
   } catch (/** @type {any} */ error) {
-    if (
-      error?.code !== "MODULE_NOT_FOUND" &&
-      error?.code !== "ERR_REQUIRE_ESM"
-    ) {
+    if (!requireErrorCodesShouldBeIgnored.has(error?.code)) {
       throw error;
     }
   }


Test patch:
diff --git a/tests/integration/__tests__/config-resolution.js b/tests/integration/__tests__/config-resolution.js
index 2885bad8b9fa..4e0cb05d05eb 100644
--- a/tests/integration/__tests__/config-resolution.js
+++ b/tests/integration/__tests__/config-resolution.js
@@ -361,3 +361,15 @@ test(".json5 config file(invalid)", async () => {
   const error = /JSON5: invalid end of input at 2:1/;
   await expect(prettier.resolveConfig(file)).rejects.toThrow(error);
 });
+
+test("support external module with `module` only `exports`", async () => {
+  const directory = path.join(
+    __dirname,
+    "../cli/config/external-config/esm-package-forbids-require",
+  );
+  const file = path.join(directory, "index.js");
+
+  await expect(prettier.resolveConfig(file)).resolves.toMatchObject({
+    printWidth: 79,
+  });
+});
diff --git a/tests/integration/cli/config/external-config/esm-package-forbids-require/index.js b/tests/integration/cli/config/external-config/esm-package-forbids-require/index.js
new file mode 100644
index 000000000000..b4639445c510
--- /dev/null
+++ b/tests/integration/cli/config/external-config/esm-package-forbids-require/index.js
@@ -0,0 +1,1 @@
+console.log("should have no semi");
diff --git a/tests/integration/cli/config/external-config/esm-package-forbids-require/node_modules/prettier-config-forbids-require/index.js b/tests/integration/cli/config/external-config/esm-package-forbids-require/node_modules/prettier-config-forbids-require/index.js
new file mode 100644
index 000000000000..613d68ff361d
--- /dev/null
+++ b/tests/integration/cli/config/external-config/esm-package-forbids-require/node_modules/prettier-config-forbids-require/index.js
@@ -0,0 +1,1 @@
+export default { printWidth: 79 }
diff --git a/tests/integration/cli/config/external-config/esm-package-forbids-require/node_modules/prettier-config-forbids-require/package.json b/tests/integration/cli/config/external-config/esm-package-forbids-require/node_modules/prettier-config-forbids-require/package.json
new file mode 100644
index 000000000000..70af1f920ddc
--- /dev/null
+++ b/tests/integration/cli/config/external-config/esm-package-forbids-require/node_modules/prettier-config-forbids-require/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "prettier-config-forbids-require",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "exports": {
+    "import": "./index.js"
+  }
+}
diff --git a/tests/integration/cli/config/external-config/esm-package-forbids-require/package.json b/tests/integration/cli/config/external-config/esm-package-forbids-require/package.json
new file mode 100644
index 000000000000..47197b278eaf
--- /dev/null
+++ b/tests/integration/cli/config/external-config/esm-package-forbids-require/package.json
@@ -0,0 +1,3 @@
+{
+  "prettier": "prettier-config-forbids-require"
+}