Skip to content

fix: apply excludedPathMatcher during external resolution - #430

Open
HugoHSun wants to merge 1 commit into
APIDevTools:mainfrom
HugoHSun:hugo/exclude-paths-during-resolution
Open

fix: apply excludedPathMatcher during external resolution#430
HugoHSun wants to merge 1 commit into
APIDevTools:mainfrom
HugoHSun:hugo/exclude-paths-during-resolution

Conversation

@HugoHSun

@HugoHSun HugoHSun commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Closes #429

Summary

External references are resolved before the existing bundle and dereference exclusion matchers run. As a result, a literal $ref below an excluded path can still be downloaded or throw a resolution error.

This adds resolve.excludedPathMatcher so callers can stop traversal before external references are fetched.

The matcher now also receives the value at the current path:

excludedPathMatcher?(path: string, value?: unknown): boolean;

The value is useful when a document contains both intentional internal references and literal external $ref properties in the same section. Callers can preserve the literal value without excluding every reference below that path.

Bundling now checks direct child values as well as containers. This ensures a matching $ref is skipped before it enters the bundle inventory.

Existing path-only matchers remain compatible because the second argument is optional.

Testing

  • Added external file and HTTP refs below excluded paths to confirm they are not resolved.
  • Added bundle coverage for excluded direct child refs.
  • Added a value-aware matcher case that resolves an internal ref while preserving an external $ref and its sibling properties.

@HugoHSun
HugoHSun marked this pull request as ready for review August 13, 2026 08:51
@jonluca

jonluca commented Aug 14, 2026

Copy link
Copy Markdown
Member

Thanks for working on this — the direction makes sense, but I don't think this is ready to merge yet. I found two blocking issues.

1. The resolver matcher receives a different path format

At lib/resolve-external.ts:83, the new matcher receives the resolver's absolute document path/URL plus fragment, for example:

/absolute/path/schema.yaml#/example

Bundle and dereference pass a root-relative JSON pointer instead:

#/example

I reproduced an exact matcher such as:

const matcher = (path: string) => path === "#/example";

working for bundle/dereference but failing during resolution, so the supposedly excluded external reference is still fetched. The inverse can also happen: a matcher such as path.includes("/example/") can match the filesystem/URL prefix and accidentally skip the entire schema when it lives under an example/ directory.

When an external document is crawled, the matcher path is also reset to that document's URL at lib/resolve-external.ts:162-164, so a logical path such as #/wrapper/example cannot exclude a nested literal ref even though the same matcher would exclude it during dereferencing.

I think resolution needs to carry a separate logical/root-relative pathFromRoot for matcher calls while retaining the existing absolute path for URI resolution. Tests should cover exact #/... matching, schemas stored beneath an example/ directory, and exclusions inside an external document.

2. This introduces a public TypeScript regression

The callable arm added to the index signature at lib/options.ts:147-152 interacts with DeepPartial at line 310. DeepPartial<function> collapses to {}, which means that under strict TypeScript:

  • invalid values such as resolve: { custom: 42 } now compile;
  • resolve: { excludedPathMatcher: "not callable" } compiles and then crashes at runtime;
  • inline matcher parameters are inferred as implicit any;
  • existing custom-resolver narrowing can stop compiling because of the new {} union arm.

DeepPartial should preserve callable types before recursively mapping object types, with consumer-facing type regression tests.

Additional edge case

Pruning a source subtree during resolution can leave external references unloaded when that same value is later reached through a nonexcluded internal $ref. For example:

{
  embedded: { child: { $ref: "child.virtual" } },
  use: { $ref: "#/embedded" }
}

If #/embedded is excluded, bundle/dereference can later visit it under #/use and fail because child.virtual was never resolved. This needs either occurrence-path-aware resolution or an explicitly documented global exclusion contract that downstream traversal also follows.

The canonical options documentation should also document resolve.excludedPathMatcher, the value argument, and the matcher path contract.

Validation

The existing implementation is otherwise clean: GitHub's cross-platform matrix is green, and locally build, typecheck, lint, formatting, 523 Node tests, and 484 browser tests all passed. The new tests use unanchored path matching, which masks the path-format issue above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Apply excludedPathMatcher before resolving external references

2 participants