Package subpath is not defined by exports

Package subpath is not defined by exports, plus ts(2307): the file exists, the exports map hides it. Here's how to import a public type instead.

Import a TypeScript type from a file that is sitting right there in node_modules, and you can get the same refusal from Node and from tsc at once:

Failed to load plugin: Error: Package subpath './proxy/processors/types' is not defined by "exports" in /home/juan/Projects/git-proxy/node_modules/@finos/git-proxy/package.json
Cannot find module '@finos/git-proxy/proxy/processors/types' or its corresponding type declarations. ts(2307)

I wrote import { PushPhase } from '@finos/git-proxy/proxy/processors/types' while building a sample scanner for GitProxy, and both tools rejected it. The file exists. The types exist. The import is still illegal.

exports is the public surface, not a hint

A package that declares an exports map in package.json no longer lets you reach into arbitrary files just because they are on disk. Node will only resolve the subpaths the map lists. Everything else is treated as if it does not exist, which is the point: the map is how a package author decides what is public API and what is an internal path they can move later.

@finos/git-proxy already declared ./plugin and ./proxy/actions. It did not declare ./proxy/processors/types. So Node’s error is exact: the package was found, the subpath was not on the list.

TypeScript follows the same map. The handbook section on package.json exports is the reason ts(2307) shows up alongside the runtime error rather than after it. Fixing the map, or importing from a path the map already exposes, fixes both.

Two ways out

The short one, and the one I used first in the sample: do not import the enum at all. PushPhase.AFTER_DIFF is the string 'AFTER_DIFF' at runtime, so { phase: 'AFTER_DIFF' } behaves identically. You lose autocomplete and typo protection, which on a value that decides whether the plugin runs at all is a real cost, but you do not depend on an entry point that does not exist.

The lasting one is to put the values on a subpath authors already import. I re-exported PushPhase, PullPhase and the option types from @finos/git-proxy/plugin, which is the module every plugin already loads for the base classes. One import line, and the public surface stays small instead of becoming “whatever happens to live in that types file.” Exposing ./proxy/processors/types wholesale would have also exported ProcessorExec, ChainElement and BuiltChains, which no plugin author should be reaching for.

If you do add a subpath, mirror the existing entries exactly, including the types / import conditional form so TypeScript and Node agree on the file:

"exports": {
  "./plugin": {
    "types": "./dist/src/plugin.d.ts",
    "import": "./dist/src/plugin.js",
    "require": "./dist/src/plugin.js"
  }
}

The package entry points docs cover the conditional keys and the way a missing condition falls through.

Why this shows up in plugin code more than in apps

An application importing its own source never goes through exports. A plugin importing its host package always does. That is why the same type is a fine relative import inside GitProxy and an illegal subpath from plugins/git-proxy-plugin-samples. It is also why a relative path like ../../src/proxy/processors/types is the wrong workaround: it resolves in the monorepo and breaks the moment the plugin is installed anywhere real.

The neighbouring error, Cannot find package ‘plugins’, is the other side of the same resolver. That one never finds a package. This one finds the package and then refuses a path inside it.

The takeaway

If a file is on disk in node_modules and both Node and tsc claim it does not exist, look at exports before you look at tsconfig. The map is the public API, TypeScript honours it, and a plugin should import host types from a path that map already exposes. The sample that hit this, and the import line it ended up with, is in how to write a GitProxy plugin.