Skip to content

Commit 9b46aac

Browse files
committed
Introduce oldestForwardsCompatibleVersion feature
1 parent 3b4807f commit 9b46aac

2 files changed

Lines changed: 69 additions & 16 deletions

File tree

apps/api-extractor-model/src/model/ApiPackage.ts

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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

4158
export 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);

apps/api-extractor-model/src/model/DeserializerContext.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,29 @@ export enum ApiJsonSchemaVersion {
1313
V_1001 = 1001,
1414

1515
/**
16-
* The current latest .api.json schema version
16+
* The current latest .api.json schema version.
17+
*
18+
* IMPORTANT: When incrementing this number, consider whether `OLDEST_SUPPORTED` or `OLDEST_FORWARDS_COMPATIBLE`
19+
* should be updated.
1720
*/
1821
LATEST = V_1001,
1922

2023
/**
21-
* The oldest .api.json schema version that is still supported for backwards compatibility
24+
* The oldest .api.json schema version that is still supported for backwards compatibility.
25+
*
26+
* This must be updated if you change to the file format and do not implement compatibility logic for
27+
* deserializing the older representation.
2228
*/
23-
OLDEST_SUPPORTED = V_1000
29+
OLDEST_SUPPORTED = V_1001,
30+
31+
/**
32+
* Used to assign `IApiPackageMetadataJson.oldestForwardsCompatibleVersion`.
33+
*
34+
* This value must be <= `ApiJsonSchemaVersion.LATEST`. It must be reset to the `LATEST` value
35+
* if the older library would not be able to deserialize your new file format. Adding a nonessential field
36+
* is generally okay. Removing, modifying, or reinterpreting existing fields is NOT safe.
37+
*/
38+
OLDEST_FORWARDS_COMPATIBLE = V_1001
2439
}
2540

2641
export class DeserializerContext {

0 commit comments

Comments
 (0)