Summary
Graphify's TypeScript/JavaScript workspace import resolver does not resolve package subpath imports through package.json exports.
For a workspace package import like:
import { value } from "@example/package-a/browser";
Graphify records an imports_from edge to a bare browser node instead of resolving the target to the exported source file from packages/package-a/package.json.
This breaks affected-file traversal and graph accuracy for monorepos that expose browser/server or other subpath entrypoints through package exports.
Minimal repro
repo/
package.json
packages/
package-a/
package.json
src/browser.ts
apps/
package-b/
src/consumer.ts
Root package.json:
{
"private": true,
"workspaces": ["packages/*", "apps/*"]
}
packages/package-a/package.json:
{
"name": "@example/package-a",
"exports": {
"./browser": {
"source": "./src/browser.ts",
"import": "./dist/esm/browser.js",
"require": "./dist/cjs/browser.js",
"types": "./dist/types/browser.d.ts"
}
}
}
packages/package-a/src/browser.ts:
export const value = "ok";
apps/package-b/src/consumer.ts:
import { value } from "@example/package-a/browser";
export const consumerValue = value;
Actual behavior
Graphify emits an import edge similar to:
{
"source": "src_consumer",
"target": "browser",
"relation": "imports_from",
"source_file": "apps/package-b/src/consumer.ts"
}
Expected behavior
Graphify should resolve the package subpath through package.json exports and emit the import edge to the exported file node, for example:
{
"source": "src_consumer",
"target": "src_browser",
"relation": "imports_from",
"source_file": "apps/package-b/src/consumer.ts"
}
The exact node id may differ, but the target should be the workspace source file behind the export map, not the bare subpath segment.
Why this matters
Many TypeScript monorepos use package subpath exports for public entrypoints, especially to separate browser-safe and server-only APIs:
{
"exports": {
"./browser": { "source": "./src/browser.ts", "import": "./dist/browser.js" },
"./server": { "source": "./src/server.ts", "import": "./dist/server.js" }
}
}
When Graphify records these as browser or server, reverse traversal and affected-file analysis become either incomplete or noisy.
Suspected cause
In the workspace package resolver, the subpath appears to be checked as a filesystem path before package export maps are considered.
Conceptually, resolving @example/package-a/browser does something equivalent to:
When that path does not resolve to a concrete file, the resolver falls back to the bare subpath string rather than consulting:
Suggested fix
For workspace package imports with a subpath:
- Locate the package root from the workspace package name.
- Read
package.json.
- Look up the subpath in
exports using the ./subpath key.
- Choose the best source target when available.
- Fall back to runtime/type targets if no source target exists.
- Only then fall back to direct filesystem probing such as
package_dir / subpath.
Suggested condition preference for source-oriented graph extraction:
source -> import -> module -> default -> require -> types
For condition objects, Graphify could recursively walk nested condition maps and return the first target matching the preferred condition list.
For wildcard exports such as:
{
"exports": {
"./utils/*": {
"source": "./src/utils/*.ts",
"import": "./dist/utils/*.js"
}
}
}
replace the * segment from the import subpath before resolving the target.
This would make Graphify match modern Node/TypeScript monorepo package boundaries while preserving the existing direct-file fallback for packages that do not define exports.
Summary
Graphify's TypeScript/JavaScript workspace import resolver does not resolve package subpath imports through
package.jsonexports.For a workspace package import like:
Graphify records an
imports_fromedge to a barebrowsernode instead of resolving the target to the exported source file frompackages/package-a/package.json.This breaks affected-file traversal and graph accuracy for monorepos that expose browser/server or other subpath entrypoints through package exports.
Minimal repro
Root
package.json:{ "private": true, "workspaces": ["packages/*", "apps/*"] }packages/package-a/package.json:{ "name": "@example/package-a", "exports": { "./browser": { "source": "./src/browser.ts", "import": "./dist/esm/browser.js", "require": "./dist/cjs/browser.js", "types": "./dist/types/browser.d.ts" } } }packages/package-a/src/browser.ts:apps/package-b/src/consumer.ts:Actual behavior
Graphify emits an import edge similar to:
{ "source": "src_consumer", "target": "browser", "relation": "imports_from", "source_file": "apps/package-b/src/consumer.ts" }Expected behavior
Graphify should resolve the package subpath through
package.jsonexportsand emit the import edge to the exported file node, for example:{ "source": "src_consumer", "target": "src_browser", "relation": "imports_from", "source_file": "apps/package-b/src/consumer.ts" }The exact node id may differ, but the target should be the workspace source file behind the export map, not the bare subpath segment.
Why this matters
Many TypeScript monorepos use package subpath exports for public entrypoints, especially to separate browser-safe and server-only APIs:
{ "exports": { "./browser": { "source": "./src/browser.ts", "import": "./dist/browser.js" }, "./server": { "source": "./src/server.ts", "import": "./dist/server.js" } } }When Graphify records these as
browserorserver, reverse traversal and affected-file analysis become either incomplete or noisy.Suspected cause
In the workspace package resolver, the subpath appears to be checked as a filesystem path before package export maps are considered.
Conceptually, resolving
@example/package-a/browserdoes something equivalent to:When that path does not resolve to a concrete file, the resolver falls back to the bare subpath string rather than consulting:
exports["./browser"]Suggested fix
For workspace package imports with a subpath:
package.json.exportsusing the./subpathkey.package_dir / subpath.Suggested condition preference for source-oriented graph extraction:
For condition objects, Graphify could recursively walk nested condition maps and return the first target matching the preferred condition list.
For wildcard exports such as:
{ "exports": { "./utils/*": { "source": "./src/utils/*.ts", "import": "./dist/utils/*.js" } } }replace the
*segment from the import subpath before resolving the target.This would make Graphify match modern Node/TypeScript monorepo package boundaries while preserving the existing direct-file fallback for packages that do not define exports.