33const fs = require ( 'fs' ) . promises ;
44const path = require ( 'path' ) ;
55
6+ // Default list of external schema files to bypass for validation and rewriting.
7+ // This constant is used as the default value for ref exceptions; can be overridden via options.refExceptions.
8+ const DEFAULT_REF_EXCEPTION_FILES = [
9+ 'spdx.schema.json' ,
10+ 'cryptography-defs.schema.json' ,
11+ 'jsf-0.82.schema.json'
12+ ] ;
13+
614function isObject ( value ) {
715 return typeof value === 'object' && value !== null ;
816}
@@ -61,13 +69,13 @@ function collectRefKeywords(obj, keys, predicate, pathStack = []) {
6169/**
6270 * Recursively walks through an object and rewrites $ref paths
6371 */
64- function rewriteRefs ( obj , schemaFiles , defsKeyword , currentSchemaName ) {
72+ function rewriteRefs ( obj , schemaFiles , defsKeyword , currentSchemaName , refExceptionSet ) {
6573 if ( typeof obj !== 'object' || obj === null ) {
6674 return obj ;
6775 }
6876
6977 if ( Array . isArray ( obj ) ) {
70- return obj . map ( item => rewriteRefs ( item , schemaFiles , defsKeyword , currentSchemaName ) ) ;
78+ return obj . map ( item => rewriteRefs ( item , schemaFiles , defsKeyword , currentSchemaName , refExceptionSet ) ) ;
7179 }
7280
7381 const newObj = { } ;
@@ -82,17 +90,22 @@ function rewriteRefs(obj, schemaFiles, defsKeyword, currentSchemaName) {
8290 const basename = path . basename ( filename ) ;
8391 const schemaName = basename . replace ( '.schema.json' , '' ) ;
8492
85- // Normalize fragment: drop leading '#' and optional leading '/'
86- let fragPath = '' ;
87- if ( fragment ) {
88- fragPath = fragment . startsWith ( '#' ) ? fragment . slice ( 1 ) : fragment ;
89- if ( fragPath . startsWith ( '/' ) ) fragPath = fragPath . slice ( 1 ) ;
93+ // If the target file is in the exception list, leave the ref as-is
94+ if ( refExceptionSet && refExceptionSet . has ( basename . toLowerCase ( ) ) ) {
95+ newObj [ key ] = value ;
96+ } else {
97+ // Normalize fragment: drop leading '#' and optional leading '/'
98+ let fragPath = '' ;
99+ if ( fragment ) {
100+ fragPath = fragment . startsWith ( '#' ) ? fragment . slice ( 1 ) : fragment ;
101+ if ( fragPath . startsWith ( '/' ) ) fragPath = fragPath . slice ( 1 ) ;
102+ }
103+
104+ // Rewrite to point to the bundled schema's definitions
105+ newObj [ key ] = fragPath
106+ ? `#/${ defsKeyword } /${ schemaName } /${ fragPath } `
107+ : `#/${ defsKeyword } /${ schemaName } ` ;
90108 }
91-
92- // Rewrite to point to the bundled schema's definitions
93- newObj [ key ] = fragPath
94- ? `#/${ defsKeyword } /${ schemaName } /${ fragPath } `
95- : `#/${ defsKeyword } /${ schemaName } ` ;
96109 }
97110 // Case 2: Internal reference within the same schema (starts with #)
98111 else if ( value . startsWith ( '#' ) ) {
@@ -104,7 +117,7 @@ function rewriteRefs(obj, schemaFiles, defsKeyword, currentSchemaName) {
104117 newObj [ key ] = value ;
105118 }
106119 } else {
107- newObj [ key ] = rewriteRefs ( value , schemaFiles , defsKeyword , currentSchemaName ) ;
120+ newObj [ key ] = rewriteRefs ( value , schemaFiles , defsKeyword , currentSchemaName , refExceptionSet ) ;
108121 }
109122 }
110123 return newObj ;
@@ -218,17 +231,22 @@ async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
218231 console . log ( `\nUsing schema version: ${ schemaVersion } ` ) ;
219232 console . log ( `Using keyword: ${ defsKeyword } ` ) ;
220233
234+ // Build exception set for external refs not to check or rewrite
235+ const refExceptionSet = new Set ( ( options . refExceptions || DEFAULT_REF_EXCEPTION_FILES ) . map ( s => s . toLowerCase ( ) ) ) ;
236+
221237 // Pre-check: external file $ref targets must exist among loaded schemas
222238 console . log ( 'Validating external $ref targets...' ) ;
223239 const allowedFiles = new Set ( [ ...schemaFiles , rootSchemaFilename ] ) ;
224240 for ( const [ name , schema ] of Object . entries ( schemas ) ) {
225241 // Only $ref can be external; $dynamicRef/$recursiveRef are JSON Pointers by spec
226- const refs = collectRefKeywords ( schema , [ '$ref' ] , ( v ) => / ^ ( . + \. s c h e m a \. j s o n ) ( # .* ) ? $ / . test ( v ) ) ;
242+ const refs = collectRefKeywords ( schema , [ '$ref' ] , ( v ) => / ^ ( \. ? . * \. s c h e m a \. j s o n ) ( # .* ) ? $ / . test ( v ) ) ;
227243 for ( const { ref, key, path : refPath } of refs ) {
228244 const m = ref . match ( / ^ ( .+ \. s c h e m a \. j s o n ) ( # .* ) ? $ / ) ;
229245 if ( ! m ) continue ;
230246 const target = m [ 1 ] ;
231247 const base = path . basename ( target ) ;
248+ // Skip validation if target file is in exceptions
249+ if ( refExceptionSet . has ( base . toLowerCase ( ) ) ) continue ;
232250 if ( ! allowedFiles . has ( base ) ) {
233251 throw new Error ( `Unresolved external ${ key } target file '${ target } ' referenced from schema '${ name } ' at '${ refPath } '` ) ;
234252 }
@@ -241,7 +259,7 @@ async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
241259 const rewrittenDefinitions = { } ;
242260 for ( const [ name , schema ] of Object . entries ( schemas ) ) {
243261 console . log ( ` Rewriting refs in ${ name } ...` ) ;
244- rewrittenDefinitions [ name ] = rewriteRefs ( schema , [ ...schemaFiles , rootSchemaFilename ] , defsKeyword , name ) ;
262+ rewrittenDefinitions [ name ] = rewriteRefs ( schema , [ ...schemaFiles , rootSchemaFilename ] , defsKeyword , name , refExceptionSet ) ;
245263 }
246264
247265 // Get the rewritten root schema
0 commit comments