fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: resolve sibling schemas from external files during bundling

When `$RefParser.bundle()` processes external files that use wrapper/redirect patterns (common in DMTF Redfish and similar large-scale OpenAPI specs), sibling schemas within versioned files are not hoisted into the root spec. This produces dangling `$ref` pointers and "Skipping unresolvable $ref" warnings.

**Affects:** 35 schemas lost when bundling the [DMTF Redfish OpenAPI spec](https://github.com/DMTF/Redfish-Publications/blob/main/openapi/openapi.yaml).

The bundler's crawl path retains the **wrapper file** URL as context when traversing the resolved schema's properties. When a local `$ref` like `#/components/schemas/SiblingSchema` is encountered inside the resolved schema, `url.resolve()` resolves it against the wrapper file — which doesn't contain the sibling. The sibling only exists in the versioned file.

**Example chain:**
1. `openapi.yaml` → `Message.v1_2_1.yaml` (HTTP)
2. `Message.v1_2_1.ya→ `ResolutionStep.yaml` (wrapper)
3. `ResolutionStep.yaml` → `ResolutionStep.v1_0_1.yaml` (versioned)
4. `ResolutionStep.v1_0_1.yaml` has `ResolutionStep_v1_0_1_ResolutionStep` with local `$ref: '#/components/schemas/ResolutionStep_v1_0_1_ResolutionType'`

`bundle()` hoists `ResolutionStep_v1_0_1_ResolutionStep` but fails to resolve its sibling `ResolutionStep_v1_0_1_ResolutionType` because it looks in `ResolutionStep.yaml` (the wrapper) instead of `ResolutionStep.v1_0_1.yaml` (the versioned file).

When `_resolve()` fails with `MissingPointerError`, try resolving the same hash fragment against all other files in the `$refs` registry. This handles the case where a local `$ref` targets a sibling schema that exists in a different file than the one retained in the crawl path.

```typescript
// Before: fail immediately
catch (error) {
if (error instanceof MissingPointerError) {
console.warn(`Skipping unresolvable $ref: ${$refPath}`);
return;
}
}

// After: try other files before giving atch (error) {
if (error instanceof MissingPointerError) {
const hash = url.getHash($refPath);
if (hash) {
const baseFile = url.stripHash($refPath);
for (const filePath of Object.keys($refs._$refs)) {
if (filePath === baseFile) continue;
try {
pointer = $refs._resolve(filePath + hash, pathFromRoot, options);
if (pointer) break;
} catch { /* try next file */ }
}
}
if (!pointer) {
console.warn(`Skipping unresolvable $ref: ${$refPath}`);
return;
}
}
}
```

Additionally, when `inventory$Ref` resolves a `$ref` that chains to a different file, the recursive crawl's path is rebased to the resolved file URL so that subsequent local `$ref`s resolve against the correct file.

Tested against the full DMTF Redfish OpenAPI specification (~2600 schemas, ~2670 paths, hundreds of external HTTP files):
- **Before:** 37 "Skipping unresolvable $ref" warnings, 35 schemas lost
- **After:** 0 warnings, all schemas correctly hoisted

- `packages/json-schema-ref-parser/src/bundle.ts` — two changes in `inventory$Ref`:
1. Fallback resolution against all `$refs` files when `MissingPointerError` occurs
2. Crawl path rebase when resolution chains to a different file

Fixes #3412

Signed-off-by: Jason Westover <jwestover@nvidia.com>

+37 -6
+37 -6
packages/json-schema-ref-parser/src/bundle.ts
··· 157 157 pointer = $refs._resolve($refPath, pathFromRoot, options); 158 158 } catch (error) { 159 159 if (error instanceof MissingPointerError) { 160 - // Log warning but continue - common in complex schema ecosystems 161 - console.warn(`Skipping unresolvable $ref: ${$refPath}`); 162 - return; 160 + // The ref couldn't be resolved in the target file. This commonly 161 + // happens when a wrapper file redirects via $ref to a versioned 162 + // file, and the bundler's crawl path retains the wrapper URL. 163 + // Try resolving the hash fragment against other files in $refs 164 + // that might contain the target schema. 165 + const hash = url.getHash($refPath); 166 + if (hash) { 167 + const baseFile = url.stripHash($refPath); 168 + for (const filePath of Object.keys($refs._$refs)) { 169 + if (filePath === baseFile) continue; 170 + try { 171 + pointer = $refs._resolve(filePath + hash, pathFromRoot, options); 172 + if (pointer) break; 173 + } catch { 174 + // try next file 175 + } 176 + } 177 + } 178 + if (!pointer) { 179 + console.warn(`Skipping unresolvable $ref: ${$refPath}`); 180 + return; 181 + } 182 + } else { 183 + throw error; 163 184 } 164 - throw error; // Re-throw unexpected errors 165 185 } 166 186 167 187 if (pointer) { ··· 217 237 inventory.push(newEntry); 218 238 inventoryLookup.add(newEntry); 219 239 220 - // Recursively crawl the resolved value 240 + // Recursively crawl the resolved value. 241 + // When the resolution followed a $ref chain to a different file, 242 + // use the resolved file as the base path so that local $ref values 243 + // (e.g. #/components/schemas/SiblingSchema) inside the resolved 244 + // value resolve against the correct file. 221 245 if (!existingEntry || external) { 246 + let crawlPath = pointer.path; 247 + 248 + const originalFile = url.stripHash($refPath); 249 + if (file !== originalFile) { 250 + crawlPath = file + url.getHash(pointer.path); 251 + } 252 + 222 253 crawl({ 223 254 $refs, 224 255 indirections: indirections + 1, ··· 227 258 key: null, 228 259 options, 229 260 parent: pointer.value, 230 - path: pointer.path, 261 + path: crawlPath, 231 262 pathFromRoot, 232 263 resolvedRefs, 233 264 visitedObjects,