@@ -25,17 +25,34 @@ export interface IApiPackageMetadataJson {
2525 * For informational purposes only.
2626 */
2727 toolPackage : string ;
28+
2829 /**
2930 * The NPM package version for the tool that wrote the *.api.json file.
3031 * For informational purposes only.
3132 */
3233 toolVersion : string ;
3334
3435 /**
35- * The * .api.json schema version . Used for determining whether the file format is
36+ * The schema version for the .api.json file format . Used for determining whether the file format is
3637 * supported, and for backwards compatibility.
3738 */
3839 schemaVersion : ApiJsonSchemaVersion ;
40+
41+ /**
42+ * To support forwards compatibility, the `oldestForwardsCompatibleVersion` field tracks the oldest schema version
43+ * whose corresponding deserializer could safely load this file.
44+ *
45+ * @remarks
46+ * Normally api-extractor-model should refuse to load a schema version that is newer than the latest version
47+ * that its deserializer understands. However, sometimes a schema change may merely introduce some new fields
48+ * without modifying or removing any existing fields. In this case, an older api-extractor-model library can
49+ * safely deserialize the newer version (by ignoring the extra fields that it doesn't recognize). The newer
50+ * serializer can use this field to communicate that.
51+ *
52+ * If present, the `oldestForwardsCompatibleVersion` must be less than or equal to
53+ * `IApiPackageMetadataJson.schemaVersion`.
54+ */
55+ oldestForwardsCompatibleVersion ?: ApiJsonSchemaVersion ;
3956}
4057
4158export interface IApiPackageJson extends IApiItemJson {
@@ -93,25 +110,45 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
93110 + `\nThe file format is not recognized; the "metadata.schemaVersion" field is missing or invalid` ) ;
94111 }
95112
96- const context : DeserializerContext = new DeserializerContext ( {
97- apiJsonFilename,
98- toolPackage : jsonObject . metadata . toolPackage ,
99- toolVersion : jsonObject . metadata . toolVersion ,
100- versionToDeserialize : jsonObject . metadata . schemaVersion
101- } ) ;
113+ const schemaVersion : number = jsonObject . metadata . schemaVersion ;
102114
103- if ( context . versionToDeserialize < ApiJsonSchemaVersion . OLDEST_SUPPORTED ) {
115+ if ( schemaVersion < ApiJsonSchemaVersion . OLDEST_SUPPORTED ) {
104116 throw new Error ( `Error loading ${ apiJsonFilename } :`
105- + `\nThe file format is version ${ context . versionToDeserialize } ,`
117+ + `\nThe file format is version ${ schemaVersion } ,`
106118 + ` whereas ${ ApiJsonSchemaVersion . OLDEST_SUPPORTED } is the oldest version supported by this tool` ) ;
107119 }
108120
109- if ( context . versionToDeserialize > ApiJsonSchemaVersion . LATEST ) {
110- throw new Error ( `Error loading ${ apiJsonFilename } :`
111- + `\nThe file format version ${ context . versionToDeserialize } was written by a newer release of`
121+ let oldestForwardsCompatibleVersion : number = schemaVersion ;
122+ if ( jsonObject . metadata . oldestForwardsCompatibleVersion ) {
123+ // Sanity check
124+ if ( jsonObject . metadata . oldestForwardsCompatibleVersion > schemaVersion ) {
125+ throw new Error ( `Error loading ${ apiJsonFilename } :`
126+ + `\nInvalid file format; "oldestForwardsCompatibleVersion" cannot be newer than "schemaVersion"` ) ;
127+ }
128+ oldestForwardsCompatibleVersion = jsonObject . metadata . oldestForwardsCompatibleVersion ;
129+ }
130+
131+ let versionToDeserialize : number = schemaVersion ;
132+ if ( versionToDeserialize > ApiJsonSchemaVersion . LATEST ) {
133+ // If the file format is too new, can we treat it as some earlier compatible version
134+ // as indicated by oldestForwardsCompatibleVersion?
135+ versionToDeserialize = Math . max ( oldestForwardsCompatibleVersion , ApiJsonSchemaVersion . LATEST ) ;
136+
137+ if ( versionToDeserialize > ApiJsonSchemaVersion . LATEST ) {
138+ // Nope, still too new
139+ throw new Error ( `Error loading ${ apiJsonFilename } :`
140+ + `\nThe file format version ${ schemaVersion } was written by a newer release of`
112141 + ` the api-extractor-model library; you may need to upgrade your software` ) ;
142+ }
113143 }
114144
145+ const context : DeserializerContext = new DeserializerContext ( {
146+ apiJsonFilename,
147+ toolPackage : jsonObject . metadata . toolPackage ,
148+ toolVersion : jsonObject . metadata . toolVersion ,
149+ versionToDeserialize : versionToDeserialize
150+ } ) ;
151+
115152 return ApiItem . deserialize ( jsonObject , context ) as ApiPackage ;
116153 }
117154
@@ -158,7 +195,8 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
158195 // In test mode, we don't write the real version, since that would cause spurious diffs whenever
159196 // the version is bumped. Instead we write a placeholder string.
160197 toolVersion : options . testMode ? '[test mode]' : options . toolVersion || packageJson . version ,
161- schemaVersion : ApiJsonSchemaVersion . LATEST
198+ schemaVersion : ApiJsonSchemaVersion . LATEST ,
199+ oldestForwardsCompatibleVersion : ApiJsonSchemaVersion . OLDEST_FORWARDS_COMPATIBLE
162200 }
163201 } as IApiPackageJson ;
164202 this . serializeInto ( jsonObject ) ;
0 commit comments