@@ -39,11 +39,20 @@ function bundle<S extends object = JSONSchema, O extends ParserOptions<S> = Pars
3939 const inventory : InventoryEntry [ ] = [ ] ;
4040 crawl < S , O > ( parser , "schema" , parser . $refs . _root$Ref . path + "#" , "#" , 0 , inventory , parser . $refs , options ) ;
4141
42+ // Get the root schema's $id (if any) for qualifying refs inside sub-schemas with their own $id
43+ const rootId =
44+ parser . schema && typeof parser . schema === "object" && "$id" in ( parser . schema as any )
45+ ? ( parser . schema as any ) . $id
46+ : undefined ;
47+
4248 // Remap all $ref pointers
43- remap < S , O > ( inventory , options ) ;
49+ remap < S , O > ( inventory , options , rootId ) ;
4450
4551 // Fix any $ref paths that traverse through other $refs (which is invalid per JSON Schema spec)
46- fixRefsThroughRefs ( inventory , parser . schema as any ) ;
52+ const bundleOptions = ( options . bundle || { } ) as BundleOptions ;
53+ if ( bundleOptions . optimizeInternalRefs !== false ) {
54+ fixRefsThroughRefs ( inventory , parser . schema as any ) ;
55+ }
4756}
4857
4958/**
@@ -209,6 +218,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
209218function remap < S extends object = JSONSchema , O extends ParserOptions < S > = ParserOptions < S > > (
210219 inventory : InventoryEntry [ ] ,
211220 options : O ,
221+ rootId ?: string ,
212222) {
213223 // Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
214224 inventory . sort ( ( a : InventoryEntry , b : InventoryEntry ) => {
@@ -256,15 +266,31 @@ function remap<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
256266 for ( const entry of inventory ) {
257267 // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
258268
269+ const bundleOpts = ( options . bundle || { } ) as BundleOptions ;
259270 if ( ! entry . external ) {
260- // This $ref already resolves to the main JSON Schema file
261- entry . $ref . $ref = entry . hash ;
271+ // This $ref already resolves to the main JSON Schema file.
272+ // When optimizeInternalRefs is false, preserve the original internal ref path
273+ // instead of rewriting it to the fully resolved hash.
274+ if ( bundleOpts . optimizeInternalRefs !== false ) {
275+ entry . $ref . $ref = entry . hash ;
276+ }
262277 } else if ( entry . file === file && entry . hash === hash ) {
263278 // This $ref points to the same value as the previous $ref, so remap it to the same path
264- entry . $ref . $ref = pathFromRoot ;
279+ if ( rootId && isInsideIdScope ( inventory , entry ) ) {
280+ // This entry is inside a sub-schema with its own $id, so a bare root-relative JSON Pointer
281+ // would be resolved relative to that $id, not the document root. Qualify with the root $id.
282+ entry . $ref . $ref = rootId + pathFromRoot ;
283+ } else {
284+ entry . $ref . $ref = pathFromRoot ;
285+ }
265286 } else if ( entry . file === file && entry . hash . indexOf ( hash + "/" ) === 0 ) {
266287 // This $ref points to a sub-value of the previous $ref, so remap it beneath that path
267- entry . $ref . $ref = Pointer . join ( pathFromRoot , Pointer . parse ( entry . hash . replace ( hash , "#" ) ) ) ;
288+ const subPath = Pointer . join ( pathFromRoot , Pointer . parse ( entry . hash . replace ( hash , "#" ) ) ) ;
289+ if ( rootId && isInsideIdScope ( inventory , entry ) ) {
290+ entry . $ref . $ref = rootId + subPath ;
291+ } else {
292+ entry . $ref . $ref = subPath ;
293+ }
268294 } else {
269295 // We've moved to a new file or new hash
270296 file = entry . file ;
@@ -409,4 +435,26 @@ function walkPath(schema: any, path: string): any {
409435 return current ;
410436}
411437
438+ /**
439+ * Checks whether the given inventory entry is located inside a sub-schema that has its own $id.
440+ * If so, root-relative JSON Pointer $refs placed at this location would be resolved against
441+ * the $id base URI rather than the document root, making them invalid.
442+ */
443+ function isInsideIdScope ( inventory : InventoryEntry [ ] , entry : InventoryEntry ) : boolean {
444+ for ( const other of inventory ) {
445+ // Skip root-level entries
446+ if ( other . pathFromRoot === "#" || other . pathFromRoot === "#/" ) {
447+ continue ;
448+ }
449+ // Check if the other entry is an ancestor of the current entry
450+ if ( entry . pathFromRoot . startsWith ( other . pathFromRoot + "/" ) ) {
451+ // Check if the ancestor's resolved value has a $id
452+ if ( other . value && typeof other . value === "object" && "$id" in other . value ) {
453+ return true ;
454+ }
455+ }
456+ }
457+ return false ;
458+ }
459+
412460export default bundle ;
0 commit comments