reffy 18.5.0 → 18.6.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reffy",
3
- "version": "18.5.0",
3
+ "version": "18.6.0",
4
4
  "description": "W3C/WHATWG spec dependencies exploration companion. Features a short set of tools to study spec references as well as WebIDL term definitions and references found in W3C specifications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,7 +46,7 @@
46
46
  "mocha": "11.1.0",
47
47
  "respec": "35.3.0",
48
48
  "respec-hljs": "2.1.1",
49
- "rollup": "4.39.0",
49
+ "rollup": "4.40.0",
50
50
  "undici": "^7.0.0"
51
51
  },
52
52
  "overrides": {
@@ -35,6 +35,25 @@
35
35
  "items": {
36
36
  "type": "object"
37
37
  }
38
+ },
39
+
40
+ "post": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "properties": {
45
+ "mod": {
46
+ "type": "string"
47
+ },
48
+ "result": {
49
+ "oneOf": [
50
+ { "type": "object" },
51
+ { "type": "array" }
52
+ ]
53
+ },
54
+ "additionalProperties": false
55
+ }
56
+ }
38
57
  }
39
58
  }
40
59
  }
@@ -75,27 +75,39 @@ const moduleFunctions = {
75
75
  .join('\n')
76
76
  },
77
77
  dfns: {
78
+ // For dfns, note we make a distinction between terms that are exported by
79
+ // default (such as CSS and Web IDL terms) and terms that editors choose to
80
+ // export explicitly. The former get reported in other details, the latter
81
+ // are the ones most likely to cause duplication issues.
78
82
  isPresent: isArrayPresent,
79
- summary: value => {
80
- const access = {};
81
- for (const dfn of value) {
82
- if (!access[dfn.access]) {
83
- access[dfn.access] = [];
84
- }
85
- access[dfn.access].push(dfn);
83
+ summary: value => [
84
+ {
85
+ access: 'explicitly exported',
86
+ dfns: value
87
+ .filter(dfn => dfn.access === 'public')
88
+ .filter(dfn => dfn.type === 'dfn' || dfn.type === 'cddl')
89
+ },
90
+ {
91
+ access: 'exported by default',
92
+ dfns: value
93
+ .filter(dfn => dfn.access === 'public')
94
+ .filter(dfn => dfn.type !== 'dfn' && dfn.type !== 'cddl')
95
+ },
96
+ {
97
+ access: 'private',
98
+ dfns: value
99
+ .filter(dfn => dfn.access !== 'public')
86
100
  }
87
- return Object.entries(access)
88
- .map(([access, dfns]) => dfns.length > 0 ?
89
- dfns.length + ' ' + access :
90
- null)
91
- .filter(found => found)
92
- .join(', ');
93
- },
101
+ ]
102
+ .map(t => t.dfns.length > 0 ? t.dfns.length + ' ' + t.access : null)
103
+ .filter(found => found)
104
+ .join(', '),
94
105
  details: value => {
95
106
  const details = value
96
107
  .filter(dfn => dfn.access === 'public')
108
+ .filter(dfn => dfn.type === 'dfn' || dfn.type === 'cddl')
97
109
  .map(dfn => '- ' + wrapTerm(dfn.linkingText[0], dfn.type, dfn.href) +
98
- (dfn.for?.length > 0 ? ' for ' + wrapTerm(dfn.for[0], dfn.type) : '') +
110
+ (dfn.for?.length > 0 ? ' for ' + wrapTerm(dfn.for[0], dfn.type): '') +
99
111
  `, type ${dfn.type}` +
100
112
  ` ([xref search](https://respec.org/xref/?term=${encodeURIComponent(dfn.linkingText[0])}))`
101
113
  );
@@ -104,7 +116,7 @@ const moduleFunctions = {
104
116
  }
105
117
  const s = details.length > 1 ? 's' : '';
106
118
  const report = ['<details>'];
107
- report.push(`<summary>${details.length} exported term${s}</summary>`);
119
+ report.push(`<summary>${details.length} explicitly exported term${s}</summary>`);
108
120
  report.push('');
109
121
  report.push(...details);
110
122
  report.push('</details>');
@@ -205,7 +217,9 @@ const moduleFunctions = {
205
217
  summary: arrayInfo
206
218
  },
207
219
  links: {
208
- isPresent: isArrayPresent,
220
+ isPresent: value =>
221
+ isArrayPresent(Object.keys(value?.rawlinks ?? {})) ||
222
+ isArrayPresent(Object.keys(value?.autolinks ?? {})),
209
223
  summary: value => ['rawlinks', 'autolinks']
210
224
  .map(prop => Object.keys(value[prop]).length > 0 ?
211
225
  Object.keys(value[prop]).length + ' ' + prop :
@@ -243,13 +257,15 @@ function arrayInfo(value) {
243
257
  }
244
258
 
245
259
  function wrapTerm(term, type, href) {
246
- let res = '';
247
260
  if (type === 'abstract-op' || type === 'dfn') {
248
- res = term;
249
- }
250
- else {
251
- res = '`' + term + '`';
261
+ if (href) {
262
+ return `[${term}](${href})`;
263
+ }
264
+ else {
265
+ return `"${term}"`;
266
+ }
252
267
  }
268
+ const res = '`' + term + '`';
253
269
  if (href) {
254
270
  return `[${res}](${href})`;
255
271
  }
@@ -312,6 +328,7 @@ export async function generateSpecReport(specResult) {
312
328
  }
313
329
  }
314
330
  if (extractsSummary.length > 0) {
331
+ extractsSummary.sort();
315
332
  summary.push(`- Spec defines:`);
316
333
  summary.push(...extractsSummary);
317
334
  }
@@ -50,7 +50,7 @@
50
50
  import fs from 'node:fs';
51
51
  import path from 'node:path';
52
52
  import { pathToFileURL } from 'node:url';
53
- import { createFolderIfNeeded } from './util.js';
53
+ import { createFolderIfNeeded, shouldSaveToFile } from './util.js';
54
54
  import csscomplete from '../postprocessing/csscomplete.js';
55
55
  import events from '../postprocessing/events.js';
56
56
  import idlnames from '../postprocessing/idlnames.js';
@@ -220,7 +220,7 @@ async function save(mod, processResult, options) {
220
220
  }
221
221
  }
222
222
 
223
- if (!options.output) {
223
+ if (!shouldSaveToFile(options)) {
224
224
  // Nothing to do if no output folder was given
225
225
  return;
226
226
  }
@@ -27,7 +27,8 @@ import {
27
27
  setupBrowser,
28
28
  teardownBrowser,
29
29
  createFolderIfNeeded,
30
- loadJSON
30
+ loadJSON,
31
+ shouldSaveToFile
31
32
  } from './util.js';
32
33
 
33
34
  import packageConfig from '../../package.json' with { type: 'json' };
@@ -188,7 +189,7 @@ async function crawlSpec(spec, crawlOptions) {
188
189
  */
189
190
  async function saveSpecResults(spec, settings) {
190
191
  settings = settings || {};
191
- if (!settings.output) {
192
+ if (!shouldSaveToFile(settings)) {
192
193
  return spec;
193
194
  }
194
195
 
@@ -337,16 +338,78 @@ async function saveSpecResults(spec, settings) {
337
338
 
338
339
 
339
340
  /**
340
- * Main method that crawls the list of specification URLs and return a structure
341
- * that full describes its title, URLs, references, and IDL definitions.
341
+ * Helper function that takes a list of specs as inputs and expands them to an
342
+ * object suitable for crawling, with as much information as possible.
342
343
  *
343
344
  * @function
344
- * @param {Array(String)} speclist List of URLs to parse
345
+ * @param {Array(String|Object)} list A list of "specs", where each spec can be
346
+ * a string that represents a spec's shortname, series shortname or URL, or an
347
+ * object that already contains appropriate information.
348
+ * @return {Array(Object)} An array of spec objects. Note: When a spec was
349
+ * already described through an object, the function returns the object as-is
350
+ * and makes no attempt at validating it.
351
+ */
352
+ function prepareListOfSpecs(list) {
353
+ return list.map(spec => {
354
+ if (typeof spec !== 'string') {
355
+ return spec;
356
+ }
357
+ let match = specs.find(s => s.url === spec || s.shortname === spec);
358
+ if (!match) {
359
+ match = specs.find(s => s.series &&
360
+ s.series.shortname === spec &&
361
+ s.series.currentSpecification === s.shortname);
362
+ }
363
+ if (match) {
364
+ return match;
365
+ }
366
+
367
+ let url = null;
368
+ try {
369
+ url = (new URL(spec)).href;
370
+ }
371
+ catch {
372
+ if (spec.endsWith('.html')) {
373
+ url = (new URL(spec, `file://${process.cwd()}/`)).href;
374
+ }
375
+ else {
376
+ const msg = `Spec ID "${spec}" can neither be interpreted as a URL, a valid shortname or a relative path to an HTML file`;
377
+ throw new Error(msg);
378
+ }
379
+ }
380
+ return {
381
+ url,
382
+ nightly: { url },
383
+ shortname: spec.replace(/[:\/\\\.]/g, ''),
384
+ series: {
385
+ shortname: spec.replace(/[:\/\\\.]/g, ''),
386
+ }
387
+ };
388
+ });
389
+ }
390
+
391
+
392
+ /**
393
+ * Crawl the provided list of specifications and return an array with the crawl
394
+ * results.
395
+ *
396
+ * Crawl options may be specified as a second parameter. The function ignores
397
+ * options that affect the output such as `output`, `markdown` or `terse`. The
398
+ * function also does not run post-processing modules that apply at the "crawl"
399
+ * level.
400
+ *
401
+ * @function
402
+ * @param {Array(String|Object)} speclist List of specs to crawl, where each
403
+ * spec can be a string that represents a spec's shortname, series shortname or
404
+ * URL, or an object that already contains appropriate information.
345
405
  * @param {Object} crawlOptions Crawl options
346
- * @return {Promise<Array(Object)} The promise to get an array of complete
347
- * specification descriptions
406
+ * @return {Promise<Array(Object)} The promise to get an array with crawl
407
+ * results.
348
408
  */
349
409
  async function crawlList(speclist, crawlOptions) {
410
+ // Expand the list of specs to spec objects suitable for crawling
411
+ speclist = prepareListOfSpecs(speclist);
412
+
350
413
  // Make a shallow copy of crawl options object since we're going
351
414
  // to modify properties in place
352
415
  crawlOptions = Object.assign({speclist}, crawlOptions);
@@ -420,6 +483,25 @@ async function crawlList(speclist, crawlOptions) {
420
483
  await teardownBrowser();
421
484
  }
422
485
 
486
+ // Merge extracts per series when necessary (CSS/IDL extracts)
487
+ for (const mod of crawlOptions.modules) {
488
+ if (mod.extractsPerSeries) {
489
+ await adjustExtractsPerSeries(results, mod.property, crawlOptions);
490
+ }
491
+ }
492
+ for (const mod of crawlOptions.post ?? []) {
493
+ if (postProcessor.extractsPerSeries(mod)) {
494
+ await adjustExtractsPerSeries(results, mod.property, crawlOptions);
495
+ }
496
+ }
497
+
498
+ // Attach a crawl summary in Markdown if so requested
499
+ if (crawlOptions.markdown || crawlOptions.summary) {
500
+ for (const res of results) {
501
+ res.crawlSummary = await generateSpecReport(res);
502
+ }
503
+ }
504
+
423
505
  return results;
424
506
  }
425
507
 
@@ -435,7 +517,7 @@ async function crawlList(speclist, crawlOptions) {
435
517
  * @return {Promise(Array)} The promise to get an updated crawl results array
436
518
  */
437
519
  async function adjustExtractsPerSeries(data, property, settings) {
438
- if (!settings.output) {
520
+ if (!shouldSaveToFile(settings)) {
439
521
  return data;
440
522
  }
441
523
 
@@ -487,7 +569,7 @@ async function adjustExtractsPerSeries(data, property, settings) {
487
569
  * @return {Promise<void>} The promise to have saved the data
488
570
  */
489
571
  async function saveResults(contents, settings) {
490
- if (!settings.output) {
572
+ if (!shouldSaveToFile(settings)) {
491
573
  return;
492
574
  }
493
575
  const indexFilename = path.join(settings.output, 'index.json');
@@ -496,62 +578,38 @@ async function saveResults(contents, settings) {
496
578
 
497
579
 
498
580
  /**
499
- * Crawls the specifications listed in the given JSON file and generates a
500
- * crawl report in the given folder.
581
+ * Run a crawl given a set of options.
582
+ *
583
+ * The set of options matches those defined in the CLI. The function crawls all
584
+ * specs by default in particular.
585
+ *
586
+ * If the `output` option is not set, the function outputs a JSON dump of the
587
+ * crawl results to the console (or a report in Markdown if the `markdown`
588
+ * option is set) and does not return anything to the caller.
589
+ *
590
+ * If the `output` option is set to the magic value `{return}`, the function
591
+ * outputs nothing but returns an object that represents the crawl results,
592
+ * with the actual results per spec stored in a `results` property.
593
+ *
594
+ * If the `output` option is set to any other value, the function interprets it
595
+ * as a folder, creates subfolders and files with crawl results in that folder,
596
+ * with a root `index.json` entry point, and does not return anything.
501
597
  *
502
598
  * @function
503
- * @param {Object} options Crawl options. Possible options are:
599
+ * @param {Object} options Crawl options. Possible options include:
504
600
  * publishedVersion, debug, output, terse, modules and specs.
505
601
  * See CLI help (node reffy.js --help) for details.
506
- * @return {Promise<void>} The promise that the crawl will have been made
602
+ * @return {Promise<void|Object>} The promise that the crawl will have been
603
+ * made along with the index of crawl results if the `output` option was set
604
+ * to the specific value `{return}`.
507
605
  */
508
606
  async function crawlSpecs(options) {
509
- function prepareListOfSpecs(list) {
510
- return list.map(spec => {
511
- if (typeof spec !== 'string') {
512
- return spec;
513
- }
514
- let match = specs.find(s => s.url === spec || s.shortname === spec);
515
- if (!match) {
516
- match = specs.find(s => s.series &&
517
- s.series.shortname === spec &&
518
- s.series.currentSpecification === s.shortname);
519
- }
520
- if (match) {
521
- return match;
522
- }
523
-
524
- let url = null;
525
- try {
526
- url = (new URL(spec)).href;
527
- }
528
- catch {
529
- if (spec.endsWith('.html')) {
530
- url = (new URL(spec, `file://${process.cwd()}/`)).href;
531
- }
532
- else {
533
- const msg = `Spec ID "${spec}" can neither be interpreted as a URL, a valid shortname or a relative path to an HTML file`;
534
- throw new Error(msg);
535
- }
536
- }
537
- return {
538
- url,
539
- nightly: { url },
540
- shortname: spec.replace(/[:\/\\\.]/g, ''),
541
- series: {
542
- shortname: spec.replace(/[:\/\\\.]/g, ''),
543
- }
544
- };
545
- });
546
- }
547
-
548
607
  const crawlIndex = options?.useCrawl ?
549
608
  await loadJSON(path.join(options.useCrawl, 'index.json')) :
550
609
  null;
551
-
552
- const requestedList = crawlIndex ? crawlIndex.results :
553
- options?.specs ? prepareListOfSpecs(options.specs) :
554
- specs;
610
+ const requestedList = crawlIndex ?
611
+ crawlIndex.results :
612
+ (options?.specs ?? specs);
555
613
 
556
614
  // Make a shallow copy of passed options parameter and expand modules
557
615
  // in place.
@@ -559,20 +617,6 @@ async function crawlSpecs(options) {
559
617
  options.modules = expandBrowserModules(options.modules);
560
618
 
561
619
  return crawlList(requestedList, options)
562
- .then(async results => {
563
- // Merge extracts per series when necessary (CSS/IDL extracts)
564
- for (const mod of options.modules) {
565
- if (mod.extractsPerSeries) {
566
- await adjustExtractsPerSeries(results, mod.property, options);
567
- }
568
- }
569
- for (const mod of options.post ?? []) {
570
- if (postProcessor.extractsPerSeries(mod)) {
571
- await adjustExtractsPerSeries(results, mod.property, options);
572
- }
573
- }
574
- return results;
575
- })
576
620
  .then(async results => {
577
621
  // Create and return a crawl index out of the results, to allow
578
622
  // post-processing modules to run.
@@ -592,13 +636,6 @@ async function crawlSpecs(options) {
592
636
  errors: results.filter(spec => !!spec.error).length
593
637
  };
594
638
 
595
- // Attach a crawl summary in Markdown if so requested
596
- if (options.markdown || options.summary) {
597
- for (const res of results) {
598
- res.crawlSummary = await generateSpecReport(res);
599
- }
600
- }
601
-
602
639
  // Return results to the console or save crawl results to an
603
640
  // index.json file
604
641
  if (options.terse) {
@@ -625,7 +662,7 @@ async function crawlSpecs(options) {
625
662
  else if (!options.output) {
626
663
  console.log(JSON.stringify(results, null, 2));
627
664
  }
628
- else {
665
+ else if (shouldSaveToFile(options)) {
629
666
  await saveResults(index, options);
630
667
  }
631
668
  return index;
@@ -636,7 +673,7 @@ async function crawlSpecs(options) {
636
673
  if (!postProcessor.appliesAtLevel(mod, 'crawl')) {
637
674
  continue;
638
675
  }
639
- const crawlResults = options.output ?
676
+ const crawlResults = shouldSaveToFile(options) ?
640
677
  await expandCrawlResult(
641
678
  crawlIndex, options.output, postProcessor.dependsOn(mod)) :
642
679
  crawlIndex;
@@ -647,25 +684,66 @@ async function crawlSpecs(options) {
647
684
  console.log();
648
685
  console.log(JSON.stringify(result, null, 2));
649
686
  }
687
+ else if (!shouldSaveToFile(options)) {
688
+ // Attach the post-processing result to the index of the
689
+ // crawl results.
690
+ crawlIndex.post = crawlIndex.post ?? [];
691
+ crawlIndex.post.push({
692
+ mod: postProcessor.getProperty(mod),
693
+ result
694
+ });
695
+ }
650
696
  }
697
+
698
+ // Function does not return anything if it already reported the
699
+ // results to the console or files. It returns the index of the
700
+ // crawl results otherwise.
701
+ if (!options.output || shouldSaveToFile(options)) {
702
+ return;
703
+ }
704
+ return crawlIndex;
651
705
  });
652
706
  }
653
707
 
654
708
 
655
- /**************************************************
656
- Export methods for use as module
657
- **************************************************/
658
- // TODO: consider more alignment between the two crawl functions or
659
- // find more explicit names to distinguish between them:
660
- // - "crawlList" takes an explicit list of specs as input, does not run the
661
- // post-processor, and returns the results without saving them to files.
662
- // - "crawlSpecs" takes options as input, runs all steps and saves results
663
- // to files (or outputs the results to the console). It does not return
664
- // anything.
709
+ /**
710
+ * Crawl a set of specs according to the given set of crawl options.
711
+ *
712
+ * The function behaves differently depending on the parameters it receives.
713
+ *
714
+ * If it receives no parameter, the function behaves as it were called with a
715
+ * single empty object as parameter.
716
+ *
717
+ * If it receives a single object as parameter, this object sets crawl options
718
+ * (essentially matching CLI options). What the function outputs or returns
719
+ * depends on the `output` option. If `output` is not set, the function outputs
720
+ * a JSON dump of the index of the crawl results to the console and returns
721
+ * nothing to the caller. If `output` is set to the "magic" value `{return}`,
722
+ * the function does not output anything but returns the index of the crawl
723
+ * results which a caller may then process in any way they wish. If `output` is
724
+ * set to any other value, it defines a folder, the function saves crawl
725
+ * results as folders and files in that folder and returns nothing.
726
+ *
727
+ * If it receives an array as first parameter, the array defines the set of
728
+ * specs that are to be crawled (each spec may be a string representing the
729
+ * spec's shortname, series shortname, or URL; or a spec object). The second
730
+ * parameter, if present, defines additional crawl options (same as above,
731
+ * except the `specs` option should not be set). The function returns an
732
+ * array of crawl results to the caller.
733
+ *
734
+ * Note the function does not apply post-processing modules that run at the
735
+ * "crawl" level when it receives an array as first parameter. It will also
736
+ * ignore crawl options that control the output such as `output`, `markdown`
737
+ * and `terse`.
738
+ */
665
739
  function crawl(...args) {
666
740
  return Array.isArray(args[0]) ?
667
741
  crawlList.apply(this, args) :
668
742
  crawlSpecs.apply(this, args);
669
743
  }
670
744
 
745
+
746
+ /**************************************************
747
+ Export crawl method for use as module
748
+ **************************************************/
671
749
  export { crawl as crawlSpecs };
package/src/lib/util.js CHANGED
@@ -1137,6 +1137,21 @@ async function getSchemaValidationFunction(schemaName) {
1137
1137
  };
1138
1138
  }
1139
1139
 
1140
+
1141
+ /**
1142
+ * Return true if the crawler should save results to files given the crawl
1143
+ * options.
1144
+ *
1145
+ * @function
1146
+ * @param {Object} crawlOptions Crawl options (optional)
1147
+ * @return {Boolean} true when the crawler should save the results to files,
1148
+ * false otherwise.
1149
+ */
1150
+ function shouldSaveToFile(crawlOptions) {
1151
+ return crawlOptions?.output && crawlOptions.output !== '{return}';
1152
+ }
1153
+
1154
+
1140
1155
  export {
1141
1156
  fetch,
1142
1157
  expandBrowserModules,
@@ -1151,5 +1166,6 @@ export {
1151
1166
  createFolderIfNeeded,
1152
1167
  getInterfaceTreeInfo,
1153
1168
  getSchemaValidationFunction,
1154
- loadJSON
1169
+ loadJSON,
1170
+ shouldSaveToFile
1155
1171
  };
@@ -14,7 +14,8 @@ import {
14
14
  getExpectedDfnFromIdlDesc } from '../cli/check-missing-dfns.js';
15
15
  import {
16
16
  isLatestLevelThatPasses,
17
- createFolderIfNeeded } from '../lib/util.js';
17
+ createFolderIfNeeded,
18
+ shouldSaveToFile } from '../lib/util.js';
18
19
 
19
20
 
20
21
  /**
@@ -379,7 +380,7 @@ async function generateIdlNames(crawl, options) {
379
380
  * @param {Object} options Crawl options ("output" will be used)
380
381
  */
381
382
  async function saveIdlNames(names, options) {
382
- if (!options?.output) {
383
+ if (!shouldSaveToFile(options)) {
383
384
  return;
384
385
  }
385
386