Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fail resolution when custom condition explicitly maps specifier '.' to nullΒ #62439

@kraenhansen

Description

@kraenhansen

Demo Repo

https://github.com/kraenhansen/typescript-issue-62439

Which of the following problems are you reporting?

Something else more complicated which I'll explain in more detail

Demonstrate the defect described above with a code sample.

When a package declares a custom condition mapped to null:

{
  // ...
  "exports": {
    ".": {
      "my-condition": null,
      "default": "./index.js"
    }
  }
}

The package shouldn't be loadable when the TS config declares the condition:

{
  // ...
  "compilerOptions": {
    "customConditions": ["my-condition"]
  },
}
// consumer.ts
import { bar } from "foo"; // Should fail πŸ’₯

Run tsc --showConfig and paste its output here

{
    "compilerOptions": {
        "module": "node16",
        "moduleResolution": "node16",
        "customConditions": [
            "my-condition"
        ],
        "target": "es2022",
        "moduleDetection": "force",
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "resolvePackageJsonExports": true,
        "resolvePackageJsonImports": true,
        "useDefineForClassFields": true
    },
    "files": [
        "./consumer.ts"
    ]
}

Run tsc --traceResolution and paste its output here

======== Resolving module 'foo' from '/redacted/typescript-issue-62438/consumer.ts'. ========
Explicitly specified module resolution kind: 'Node16'.
Resolving in ESM mode with conditions 'import', 'types', 'node', 'my-condition'.
File '/redacted/typescript-issue-62438/package.json' exists according to earlier cached lookups.
Loading module 'foo' from 'node_modules' folder, target file types: TypeScript, JavaScript, Declaration, JSON.
Searching all ancestor node_modules directories for preferred extensions: TypeScript, Declaration.
Found 'package.json' at '/redacted/typescript-issue-62438/node_modules/foo/package.json'.
Entering conditional exports.
Matched 'exports' condition 'my-condition'.
package.json scope '/redacted/typescript-issue-62438/node_modules/foo' explicitly maps specifier '.' to null.
Failed to resolve under condition 'my-condition'.
Matched 'exports' condition 'default'.
Using 'exports' subpath '.' with target './index.js'.
File name '/redacted/typescript-issue-62438/node_modules/foo/index.js' has a '.js' extension - stripping it.
File '/redacted/typescript-issue-62438/node_modules/foo/index.ts' does not exist.
File '/redacted/typescript-issue-62438/node_modules/foo/index.tsx' does not exist.
File '/redacted/typescript-issue-62438/node_modules/foo/index.d.ts' exists - use it as a name resolution result.
'package.json' does not have a 'peerDependencies' field.
Resolved under condition 'default'.
Exiting conditional exports.
Resolving real path for '/redacted/typescript-issue-62438/node_modules/foo/index.d.ts', result '/redacted/typescript-issue-62438/foo/index.d.ts'.
======== Module name 'foo' was successfully resolved to '/redacted/typescript-issue-62438/foo/index.d.ts' with Package ID 'foo/[email protected]'. ========

(I skipped unrelated resolutions)

Paste the package.json of the importing module, if it exists

{
  "name": "ts-custom-condition-bug",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "start": "node consumer.ts",
    "start:condition": "node --conditions my-condition consumer.ts",
    "tsc": "tsc --noEmit",
    "trace": "tsc --noEmit --traceResolution"
  },
  "devDependencies": {
    "typescript": "5.9.2"
  },
  "dependencies": {
    "foo": "./foo"
  }
}

Paste the package.json of the target module, if it exists

{
  "name": "foo",
  "version": "0.1.0",
  "type": "module",
  "exports": {
    ".": {
      "my-condition": null,
      "default": "./index.js"
    }
  }
}

Any other comments can go here

If a module declares an export condition with the null value and the resolution of this module match this export condition, the resolution should fail.
This however is not the behavior of the TypeScript compiler (v5.9.2).
Instead of failing the resolution falls back on the default condition (see --traceResolution output below)

======== Resolving module 'foo' from '/redacted/typescript-issue-62438/consumer.ts'. ========
Explicitly specified module resolution kind: 'Node16'.
Resolving in ESM mode with conditions 'import', 'types', 'node', 'my-condition'.
File '/redacted/typescript-issue-62438/package.json' exists according to earlier cached lookups.
Loading module 'foo' from 'node_modules' folder, target file types: TypeScript, JavaScript, Declaration, JSON.
Searching all ancestor node_modules directories for preferred extensions: TypeScript, Declaration.
Found 'package.json' at '/redacted/typescript-issue-62438/node_modules/foo/package.json'.
Entering conditional exports.
Matched 'exports' condition 'my-condition'.
package.json scope '/redacted/typescript-issue-62438/node_modules/foo' explicitly maps specifier '.' to null.
Failed to resolve under condition 'my-condition'.
Matched 'exports' condition 'default'.
Using 'exports' subpath '.' with target './index.js'.
File name '/redacted/typescript-issue-62438/node_modules/foo/index.js' has a '.js' extension - stripping it.
File '/redacted/typescript-issue-62438/node_modules/foo/index.ts' does not exist.
File '/redacted/typescript-issue-62438/node_modules/foo/index.tsx' does not exist.
File '/redacted/typescript-issue-62438/node_modules/foo/index.d.ts' exists - use it as a name resolution result.
'package.json' does not have a 'peerDependencies' field.
Resolved under condition 'default'.
Exiting conditional exports.
Resolving real path for '/redacted/typescript-issue-62438/node_modules/foo/index.d.ts', result '/redacted/typescript-issue-62438/foo/index.d.ts'.
======== Module name 'foo' was successfully resolved to '/redacted/typescript-issue-62438/foo/index.d.ts' with Package ID 'foo/[email protected]'. ========

Notice how this behavior is different from Node.js, when passing the condition (--conditions my-condition):

> node --conditions my-condition consumer.ts

node:internal/modules/esm/resolve:313
  return new ERR_PACKAGE_PATH_NOT_EXPORTED(
         ^

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in /redacted/typescript-issue-62438/node_modules/foo/package.json imported from /redacted/typescript-issue-62438/consumer.ts
    at exportsNotFound (node:internal/modules/esm/resolve:313:10)
    at packageExportsResolve (node:internal/modules/esm/resolve:603:13)
    at packageResolve (node:internal/modules/esm/resolve:773:12)
    at moduleResolve (node:internal/modules/esm/resolve:853:18)
    at defaultResolve (node:internal/modules/esm/resolve:983:11)
    at #cachedDefaultResolve (node:internal/modules/esm/loader:717:20)
    at ModuleLoader.resolve (node:internal/modules/esm/loader:694:38)
    at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:308:38)
    at ModuleJob._link (node:internal/modules/esm/module_job:183:49) {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}

Node.js v22.19.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions