versioned-d.ts-tools 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ export interface WhatsNewConfig {
10
10
  excludedFieldPatterns?: string[];
11
11
  includeStaticFields?: boolean;
12
12
  includeEnums?: boolean;
13
+ filterUnionAdditions?: boolean;
13
14
  linkConfigs?: LinkConfig[];
14
15
  replacements?: any[];
15
16
  }
@@ -21,6 +22,7 @@ export interface MarkdownOptions {
21
22
  excludedFieldPatterns?: string[];
22
23
  includeStaticFields?: boolean;
23
24
  includeEnums?: boolean;
25
+ filterUnionAdditions?: boolean;
24
26
  }
25
27
  export declare const DEFAULT_LINK_CONFIGS: LinkConfig[];
26
28
  declare enum ClassType {
@@ -57,8 +59,24 @@ export declare class APISet {
57
59
  constructor();
58
60
  addClass(clas: ClassStruct): void;
59
61
  containsClass(clas: ClassStruct): boolean;
62
+ /**
63
+ * Checks if two field declaration strings are equivalent except for additional union types.
64
+ * This helps filter out noise from new union options being added to existing parameters.
65
+ */
66
+ private isOnlyUnionAddition;
60
67
  containsField(clas: ClassStruct, field: FieldStruct): boolean;
61
- diff(other: APISet): APISet;
68
+ containsFieldOrUnionAddition(clas: ClassStruct, field: FieldStruct): boolean;
69
+ /**
70
+ * Checks if a class name appears only in union type contexts and not as standalone types.
71
+ * Returns true if the class only appears in union expressions like "Range | RangeAreas"
72
+ * and doesn't appear as standalone return types or parameters.
73
+ */
74
+ /**
75
+ * Checks if a class with filtered fields should be excluded due to union-only changes.
76
+ * This is called during markdown generation after field exclusions have been applied.
77
+ */
78
+ private shouldExcludeClassForUnionFiltering;
79
+ diff(other: APISet, filterUnionAdditions?: boolean): APISet;
62
80
  getAsDTS(): string;
63
81
  /**
64
82
  * Generates markdown documentation for API differences.
@@ -165,27 +165,167 @@ class APISet {
165
165
  containsClass(clas) {
166
166
  return this.api.some(element => element.declarationString === clas.declarationString);
167
167
  }
168
+ /**
169
+ * Checks if two field declaration strings are equivalent except for additional union types.
170
+ * This helps filter out noise from new union options being added to existing parameters.
171
+ */
172
+ isOnlyUnionAddition(newDeclaration, oldDeclaration) {
173
+ // If they're identical, no difference at all
174
+ if (newDeclaration === oldDeclaration) {
175
+ return false;
176
+ }
177
+ // Quick check: if the method/field names are different, this is not a union addition
178
+ // Extract method/field name from the beginning of each declaration
179
+ const getMethodName = (decl) => {
180
+ const match = decl.match(/^\s*(\w+)\s*[\(:]/) || decl.match(/^\s*(\w+)\s*\??\s*:/);
181
+ return match ? match[1] : '';
182
+ };
183
+ const oldMethodName = getMethodName(oldDeclaration);
184
+ const newMethodName = getMethodName(newDeclaration);
185
+ // If method names are different, this is a new method, not a union addition
186
+ if (oldMethodName !== newMethodName || !oldMethodName) {
187
+ return false;
188
+ }
189
+ // More sophisticated approach: extract and compare union types from specific parameters
190
+ // This handles cases where union additions are in the middle of parameter lists
191
+ // Normalize whitespace first
192
+ const oldNormalized = oldDeclaration.replace(/\s+/g, ' ').trim();
193
+ const newNormalized = newDeclaration.replace(/\s+/g, ' ').trim();
194
+ // Find all union type patterns in both declarations
195
+ // Pattern matches: paramName: Type1 | Type2 | Type3 or paramName?: Type1 | Type2 | Type3
196
+ const unionPattern = /(\w+\??\s*:\s*)([^;,)]+)/g;
197
+ const oldUnions = new Map();
198
+ const newUnions = new Map();
199
+ let match;
200
+ // Extract union types from old declaration
201
+ unionPattern.lastIndex = 0;
202
+ while ((match = unionPattern.exec(oldNormalized)) !== null) {
203
+ const paramName = match[1].trim();
204
+ const unionString = match[2].trim();
205
+ const unionTypes = unionString.split('|').map(t => t.trim().replace(/^["']|["']$/g, ''));
206
+ oldUnions.set(paramName, unionTypes);
207
+ }
208
+ // Extract union types from new declaration
209
+ unionPattern.lastIndex = 0;
210
+ while ((match = unionPattern.exec(newNormalized)) !== null) {
211
+ const paramName = match[1].trim();
212
+ const unionString = match[2].trim();
213
+ const unionTypes = unionString.split('|').map(t => t.trim().replace(/^["']|["']$/g, ''));
214
+ newUnions.set(paramName, unionTypes);
215
+ }
216
+ // Check if we found any union parameters
217
+ if (oldUnions.size === 0 || newUnions.size === 0) {
218
+ return false;
219
+ }
220
+ // Check if the parameters are the same except for union additions
221
+ let hasUnionAddition = false;
222
+ for (const [paramName, oldTypes] of oldUnions) {
223
+ const newTypes = newUnions.get(paramName);
224
+ if (!newTypes) {
225
+ // Parameter removed, not a union addition
226
+ return false;
227
+ }
228
+ // Check if all old types are present in new types
229
+ const allOldTypesPresent = oldTypes.every(oldType => newTypes.some(newType => newType === oldType));
230
+ if (!allOldTypesPresent) {
231
+ // Types were removed or changed, not just added
232
+ return false;
233
+ }
234
+ // Check if new types were added
235
+ if (newTypes.length > oldTypes.length) {
236
+ hasUnionAddition = true;
237
+ }
238
+ }
239
+ // Check for any completely new parameters (not allowed for union addition)
240
+ for (const paramName of newUnions.keys()) {
241
+ if (!oldUnions.has(paramName)) {
242
+ return false;
243
+ }
244
+ }
245
+ return hasUnionAddition;
246
+ }
168
247
  containsField(clas, field) {
169
248
  const targetClass = this.api.find(element => element.declarationString === clas.declarationString);
170
249
  return targetClass ? targetClass.fields.some(thisField => thisField.declarationString === field.declarationString) : false;
171
250
  }
251
+ containsFieldOrUnionAddition(clas, field) {
252
+ const targetClass = this.api.find(element => element.declarationString === clas.declarationString);
253
+ if (!targetClass) {
254
+ return false;
255
+ }
256
+ return targetClass.fields.some(thisField => {
257
+ // Exact match
258
+ if (thisField.declarationString === field.declarationString) {
259
+ return true;
260
+ }
261
+ // Check if it's only a union addition
262
+ return this.isOnlyUnionAddition(field.declarationString, thisField.declarationString);
263
+ });
264
+ }
265
+ /**
266
+ * Checks if a class name appears only in union type contexts and not as standalone types.
267
+ * Returns true if the class only appears in union expressions like "Range | RangeAreas"
268
+ * and doesn't appear as standalone return types or parameters.
269
+ */
270
+ /**
271
+ * Checks if a class with filtered fields should be excluded due to union-only changes.
272
+ * This is called during markdown generation after field exclusions have been applied.
273
+ */
274
+ shouldExcludeClassForUnionFiltering(clas, originalFields, excludedFieldPatterns = [], excludedFieldNames = []) {
275
+ // If no fields in the class, don't exclude for union reasons
276
+ if (originalFields.length === 0) {
277
+ return false;
278
+ }
279
+ // First filter out excluded fields to see what would actually be displayed
280
+ const visibleFields = originalFields.filter(field => {
281
+ const isExcludedByPattern = excludedFieldPatterns.some(pattern => {
282
+ try {
283
+ return new RegExp(pattern, 'g').test(field.declarationString);
284
+ }
285
+ catch (error) {
286
+ return false;
287
+ }
288
+ });
289
+ const isExcludedByName = excludedFieldNames.includes(field.name);
290
+ return !isExcludedByPattern && !isExcludedByName;
291
+ });
292
+ // If no fields would be visible after exclusions, and all original fields contain unions,
293
+ // then this class likely appears only due to union additions in other classes
294
+ if (visibleFields.length === 0 && originalFields.every(field => field.declarationString.includes('|'))) {
295
+ return true;
296
+ }
297
+ return false;
298
+ }
172
299
  // finds the new fields and classes
173
- diff(other) {
300
+ diff(other, filterUnionAdditions = false) {
174
301
  const diffAPI = new APISet();
175
302
  this.api.forEach((element) => {
176
303
  if (other.containsClass(element)) {
177
304
  let classShell = null;
178
305
  element.fields.forEach((field) => {
179
- if (!other.containsField(element, field)) {
306
+ let fieldExists;
307
+ if (filterUnionAdditions) {
308
+ // Use union-aware field comparison when filtering is enabled
309
+ fieldExists = other.containsFieldOrUnionAddition(element, field);
310
+ }
311
+ else {
312
+ // Use exact match comparison
313
+ fieldExists = other.containsField(element, field);
314
+ }
315
+ if (!fieldExists) {
180
316
  if (classShell === null) {
181
317
  classShell = element.copyWithoutFields();
182
- diffAPI.addClass(classShell);
183
318
  }
184
319
  classShell.fields.push(field);
185
320
  }
186
321
  });
322
+ // Only add the class to diffAPI if it has fields after filtering
323
+ if (classShell !== null && classShell.fields.length > 0) {
324
+ diffAPI.addClass(classShell);
325
+ }
187
326
  }
188
327
  else {
328
+ // Add new classes to the diff
189
329
  diffAPI.addClass(element);
190
330
  }
191
331
  });
@@ -242,19 +382,15 @@ class APISet {
242
382
  const isEnum = clas.type === ClassType.Enum;
243
383
  if ((finalOptions.includeEnums || !isEnum) && !isExcludedByPattern) {
244
384
  const className = clas.getClassName();
245
- // Special handling for <global> - no link
246
- if (className === "<global>") {
247
- output += "|*global*|";
248
- }
249
- else {
250
- output += "|[" + className + "]("
251
- + buildClassLink(finalOptions.relativePath, className, finalOptions.linkConfigs) + ")|";
385
+ // Apply union filtering if enabled - check if this class should be filtered out BEFORE field filtering
386
+ if (finalOptions.filterUnionAdditions) {
387
+ if (this.shouldExcludeClassForUnionFiltering(clas, clas.fields, finalOptions.excludedFieldPatterns || [], finalOptions.excludedFieldNames || [])) {
388
+ // Skip this class as it only contains union-related changes
389
+ return;
390
+ }
252
391
  }
253
- // Ignore the following:
254
- // - Fields matching excluded patterns (configurable, no defaults - all fields included unless explicitly excluded)
255
- // - Excluded field names (configurable, no defaults - all field names included unless explicitly excluded)
256
- // - Static fields (configurable via includeStaticFields, default: true - static fields included by default)
257
- let filteredFields = clas.fields.filter((field) => {
392
+ // Apply field filtering after union filtering check
393
+ const filteredFields = clas.fields.filter((field) => {
258
394
  // Check if field matches any excluded pattern
259
395
  // Note: Test against field declaration string, just like the original code
260
396
  const isExcludedByPattern = (finalOptions.excludedFieldPatterns || []).some(pattern => {
@@ -272,6 +408,19 @@ class APISet {
272
408
  !(finalOptions.excludedFieldNames || []).includes(field.name) &&
273
409
  (finalOptions.includeStaticFields || !isStaticField));
274
410
  });
411
+ // Special handling for <global> - no link
412
+ if (className === "<global>") {
413
+ output += "|*global*|";
414
+ }
415
+ else {
416
+ output += "|[" + className + "]("
417
+ + buildClassLink(finalOptions.relativePath, className, finalOptions.linkConfigs) + ")|";
418
+ }
419
+ // Ignore the following:
420
+ // - Fields matching excluded patterns (configurable, no defaults - all fields included unless explicitly excluded)
421
+ // - Excluded field names (configurable, no defaults - all field names included unless explicitly excluded)
422
+ // - Static fields (configurable via includeStaticFields, default: true - static fields included by default)
423
+ // (filteredFields already computed above)
275
424
  let first = true;
276
425
  if (filteredFields.length > 0) {
277
426
  filteredFields.forEach((field) => {
@@ -311,7 +460,8 @@ class APISet {
311
460
  });
312
461
  }
313
462
  else {
314
- output += "||\n";
463
+ // When all fields are excluded, still show the class description
464
+ output += "|" + removeAtLink(extractFirstSentenceFromComment(clas.comment)) + "|\n";
315
465
  }
316
466
  }
317
467
  });
@@ -114,9 +114,9 @@ if (require.main === module) {
114
114
  const args = process.argv.slice(2);
115
115
  const hasHelp = args.find((x) => x === "-?" || x === "--help");
116
116
  if ((args.length !== 3 && args.length !== 4) || hasHelp) {
117
- console.log("usage: node version-remover [source d.ts] [output file name] [version string] [optional config file]");
118
- console.log("example: node version-remover excel.d.ts excel_1_7.d.ts \"Api set: ExcelApi 1.8\"");
119
- console.log("example: node version-remover excel.d.ts excel_1_18.d.ts \"Api set: ExcelApi 1.19\" my-config.json");
117
+ console.log("usage: version-remover [source d.ts] [output file name] [version string] [optional config file]");
118
+ console.log("example: version-remover excel.d.ts excel_1_7.d.ts \"Api set: ExcelApi 1.8\"");
119
+ console.log("example: version-remover excel.d.ts excel_1_18.d.ts \"Api set: ExcelApi 1.19\" my-config.json");
120
120
  console.log("");
121
121
  console.log("Note: For specific API sets that require custom replacements, you may need to create");
122
122
  console.log("configuration files to handle type union cleanups and parameter removals.");
@@ -12,6 +12,7 @@ export interface WhatsNewOptions {
12
12
  excludedFieldPatterns?: string[];
13
13
  includeStaticFields?: boolean;
14
14
  includeEnums?: boolean;
15
+ filterUnionAdditions?: boolean;
15
16
  }
16
17
  /**
17
18
  * Generates "what's new" documentation by comparing two TypeScript definition files.
package/dist/whats-new.js CHANGED
@@ -40,7 +40,7 @@ const dts_utilities_1 = require("./dts-utilities");
40
40
  /**
41
41
  * Loads configuration from a JSON file
42
42
  * @param configFilePath Path to the configuration file
43
- * @returns Configuration object with linkConfigs, excludedFieldNames, excludedClassPatterns, excludedFieldPatterns, includeStaticFields, and includeEnums
43
+ * @returns Configuration object with linkConfigs, excludedFieldNames, excludedClassPatterns, excludedFieldPatterns, includeStaticFields, includeEnums, and filterUnionAdditions
44
44
  */
45
45
  function loadWhatsNewConfig(configFilePath) {
46
46
  try {
@@ -67,7 +67,8 @@ function loadWhatsNewConfig(configFilePath) {
67
67
  excludedClassPatterns: config.excludedClassPatterns,
68
68
  excludedFieldPatterns: config.excludedFieldPatterns,
69
69
  includeStaticFields: config.includeStaticFields,
70
- includeEnums: config.includeEnums
70
+ includeEnums: config.includeEnums,
71
+ filterUnionAdditions: config.filterUnionAdditions
71
72
  };
72
73
  }
73
74
  catch (error) {
@@ -87,7 +88,8 @@ function generateWhatsNew(options) {
87
88
  let effectiveExcludedFieldPatterns = options.excludedFieldPatterns;
88
89
  let effectiveIncludeStaticFields = options.includeStaticFields;
89
90
  let effectiveIncludeEnums = options.includeEnums;
90
- if (options.configFilePath && (!options.linkConfigs || !options.excludedFieldNames || !options.excludedClassPatterns || !options.excludedFieldPatterns || options.includeStaticFields === undefined || options.includeEnums === undefined)) {
91
+ let effectiveFilterUnionAdditions = options.filterUnionAdditions;
92
+ if (options.configFilePath && (!options.linkConfigs || !options.excludedFieldNames || !options.excludedClassPatterns || !options.excludedFieldPatterns || options.includeStaticFields === undefined || options.includeEnums === undefined || options.filterUnionAdditions === undefined)) {
91
93
  const config = loadWhatsNewConfig(options.configFilePath);
92
94
  if (!options.linkConfigs && config.linkConfigs) {
93
95
  effectiveLinkConfigs = config.linkConfigs;
@@ -108,13 +110,16 @@ function generateWhatsNew(options) {
108
110
  // Default to true when loading from config (inclusive by default)
109
111
  effectiveIncludeEnums = config.includeEnums !== undefined ? config.includeEnums : true;
110
112
  }
113
+ if (options.filterUnionAdditions === undefined && config.filterUnionAdditions !== undefined) {
114
+ effectiveFilterUnionAdditions = config.filterUnionAdditions;
115
+ }
111
116
  }
112
117
  // read whole files
113
118
  let wholeRelease = fsx.readFileSync(options.oldDtsPath).toString();
114
119
  let wholePreview = fsx.readFileSync(options.newDtsPath).toString();
115
120
  const releaseAPI = (0, dts_utilities_1.parseDTS)("release", wholeRelease);
116
121
  const previewAPI = (0, dts_utilities_1.parseDTS)("preview", wholePreview);
117
- const diffAPI = previewAPI.diff(releaseAPI);
122
+ const diffAPI = previewAPI.diff(releaseAPI, effectiveFilterUnionAdditions);
118
123
  if (!fsx.existsSync(options.outputPath + ".md")) {
119
124
  fsx.createFileSync(options.outputPath + ".md");
120
125
  }
@@ -125,7 +130,8 @@ function generateWhatsNew(options) {
125
130
  excludedClassPatterns: effectiveExcludedClassPatterns,
126
131
  excludedFieldPatterns: effectiveExcludedFieldPatterns,
127
132
  includeStaticFields: effectiveIncludeStaticFields,
128
- includeEnums: effectiveIncludeEnums
133
+ includeEnums: effectiveIncludeEnums,
134
+ filterUnionAdditions: effectiveFilterUnionAdditions
129
135
  }));
130
136
  }
131
137
  // CLI entry point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versioned-d.ts-tools",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Tools for managing versioned TypeScript definition files",
5
5
  "main": "dist/index.js",
6
6
  "bin": {