@@ -43,6 +43,7 @@ import loadSpec from "./load-spec.js";
43
43
import computeShortname from "./compute-shortname.js" ;
44
44
import Octokit from "./octokit.js" ;
45
45
import ThrottledQueue from "./throttled-queue.js" ;
46
+ import fetchJSON from "./fetch-json.js" ;
46
47
47
48
// Map spec statuses returned by Specref to those used in specs
48
49
// Note we typically won't get /TR statuses from Specref, since all /TR URLs
@@ -55,8 +56,6 @@ const specrefStatusMapping = {
55
56
"cg-draft" : "Draft Community Group Report"
56
57
} ;
57
58
58
- const fetchQueue = new ThrottledQueue ( { maxParallel : 2 } ) ;
59
-
60
59
async function useLastInfoForDiscontinuedSpecs ( specs ) {
61
60
const results = { } ;
62
61
for ( const spec of specs ) {
@@ -95,31 +94,22 @@ async function fetchInfoFromW3CApi(specs, options) {
95
94
}
96
95
97
96
const url = `https://api.w3.org/specifications/${ spec . shortname } /versions/latest` ;
98
- const res = await fetchQueue . runThrottled ( fetch , url , options ) ;
99
- if ( res . status === 404 ) {
100
- return ;
101
- }
102
- if ( res . status !== 200 ) {
103
- throw new Error ( `W3C API returned an error, status code is ${ res . status } , url was ${ url } ` ) ;
104
- }
105
-
106
- // Has the shortname changed from a W3C perspective?
107
- if ( res . redirected ) {
108
- const match = res . url . match ( / \/ s p e c i f i c a t i o n s \/ ( [ ^ \/ ] + ) \/ / ) ;
109
- const w3cShortname = match ? match [ 1 ] : '' ;
110
- if ( w3cShortname !== spec . shortname ) {
111
- throw new Error ( `W3C API redirects "${ spec . shortname } " to ` +
112
- `"${ w3cShortname } ", update the shortname!` ) ;
113
- }
114
- }
115
-
116
- try {
117
- const body = await res . json ( ) ;
118
- return body ;
119
- }
120
- catch ( err ) {
121
- throw new Error ( "W3C API returned invalid JSON" ) ;
122
- }
97
+ const body = await fetchJSON ( url , options ) ;
98
+
99
+ // The shortname of the specification may have changed. In such cases, the
100
+ // W3C API silently redirects to the info for the new shortname, whereas we
101
+ // want to make sure we use the latest shortname in browser-specs. The
102
+ // actual shortname used by the W3C API does not appear explicitly in the
103
+ // response to a "/versions/latest" request, but it appears implicitly in
104
+ // the "_links/specification/href" URL.
105
+ const match = body . _links . specification . href . match ( / \/ s p e c i f i c a t i o n s \/ ( [ ^ \/ ] + ) $ / ) ;
106
+ const shortname = match [ 1 ] ;
107
+ if ( shortname !== spec . shortname ) {
108
+ throw new Error ( `W3C API redirects "${ spec . shortname } " to ` +
109
+ `"${ shortname } ", update the shortname!` ) ;
110
+ }
111
+
112
+ return body ;
123
113
} ) ) ;
124
114
125
115
const seriesShortnames = new Set ( ) ;
@@ -153,28 +143,15 @@ async function fetchInfoFromW3CApi(specs, options) {
153
143
// Fetch info on the series
154
144
const seriesInfo = await Promise . all ( [ ...seriesShortnames ] . map ( async shortname => {
155
145
const url = `https://api.w3.org/specification-series/${ shortname } ` ;
156
- const res = await fetchQueue . runThrottled ( fetch , url , options ) ;
157
- if ( res . status === 404 ) {
158
- return ;
159
- }
160
- if ( res . status !== 200 ) {
161
- throw new Error ( `W3C API returned an error, status code is ${ res . status } ` ) ;
162
- }
163
- try {
164
- const body = await res . json ( ) ;
165
-
166
- // The CSS specs and the CSS snapshots have different series shortnames for
167
- // us ("CSS" vs. "css"), but the W3C API is case-insentive, mixes the two
168
- // series, and claims that the series shortname is "CSS" or "css"
169
- // depending on which spec got published last. Let's get back to the
170
- // shortname we requested.
171
- body . shortname = shortname ;
172
-
173
- return body ;
174
- }
175
- catch ( err ) {
176
- throw new Error ( "W3C API returned invalid JSON" ) ;
177
- }
146
+ const body = await fetchJSON ( url , options ) ;
147
+
148
+ // The CSS specs and the CSS snapshots have different series shortnames for
149
+ // us ("CSS" vs. "css"), but the W3C API is case-insentive, mixes the two
150
+ // series, and claims that the series shortname is "CSS" or "css"
151
+ // depending on which spec got published last. Let's get back to the
152
+ // shortname we requested.
153
+ body . shortname = shortname ;
154
+ return body ;
178
155
} ) ) ;
179
156
180
157
results . __series = { } ;
@@ -207,6 +184,36 @@ async function fetchInfoFromW3CApi(specs, options) {
207
184
return results ;
208
185
}
209
186
187
+ async function fetchInfoFromWHATWG ( specs , options ) {
188
+ const whatwgRe = / \. w h a t w g \. o r g / ;
189
+ if ( ! specs . find ( spec => spec . url . match ( whatwgRe ) ) ) {
190
+ return { } ;
191
+ }
192
+
193
+ // Note: The WHATWG biblio.json file could also be used, but we're going to
194
+ // need the workstreams database in any case in fetch-groups, so let's fetch
195
+ // the database directly (this will put it in cache for fetch-groups)
196
+ const url = 'https://raw.githubusercontent.com/whatwg/sg/main/db.json' ;
197
+ const db = await fetchJSON ( url , options ) ;
198
+ const standards = db . workstreams . map ( ws => ws . standards ) . flat ( ) ;
199
+
200
+ const specInfo = { } ;
201
+ for ( const spec of specs ) {
202
+ if ( ! spec . url . match ( / \. w h a t w g \. o r g / ) ) {
203
+ continue ;
204
+ }
205
+ const entry = standards . find ( std => std . href === spec . url ) ;
206
+ if ( ! entry ) {
207
+ console . warn ( `[warning] WHATWG spec at ${ spec . url } not found in WHATWG database` ) ;
208
+ continue ;
209
+ }
210
+ specInfo [ spec . shortname ] = {
211
+ nightly : { url : spec . url , status : 'Living Standard' } ,
212
+ title : entry . name
213
+ } ;
214
+ }
215
+ return specInfo ;
216
+ }
210
217
211
218
async function fetchInfoFromSpecref ( specs , options ) {
212
219
function chunkArray ( arr , len ) {
@@ -224,11 +231,7 @@ async function fetchInfoFromSpecref(specs, options) {
224
231
// API does not return the "source" field, so we need to retrieve the list
225
232
// ourselves from Specref's GitHub repository.
226
233
const specrefBrowserspecsUrl = "https://raw.githubusercontent.com/tobie/specref/main/refs/browser-specs.json" ;
227
- const browserSpecsResponse = await fetch ( specrefBrowserspecsUrl , options ) ;
228
- if ( browserSpecsResponse . status !== 200 ) {
229
- throw new Error ( `Could not retrieve specs contributed by browser-specs to Speref, status code is ${ browserSpecsResponse . status } ` ) ;
230
- }
231
- const browserSpecs = await browserSpecsResponse . json ( ) ;
234
+ const browserSpecs = await fetchJSON ( specrefBrowserspecsUrl , options ) ;
232
235
specs = specs . filter ( spec => ! browserSpecs [ spec . shortname . toUpperCase ( ) ] ) ;
233
236
234
237
// Browser-specs now acts as source for Specref for the WICG specs and W3C
@@ -244,18 +247,7 @@ async function fetchInfoFromSpecref(specs, options) {
244
247
const chunksRes = await Promise . all ( chunks . map ( async chunk => {
245
248
let specrefUrl = "https://api.specref.org/bibrefs?refs=" +
246
249
chunk . map ( spec => spec . shortname ) . join ( ',' ) ;
247
-
248
- const res = await fetchQueue . runThrottled ( fetch , specrefUrl , options ) ;
249
- if ( res . status !== 200 ) {
250
- throw new Error ( `Could not query Specref, status code is ${ res . status } ` ) ;
251
- }
252
- try {
253
- const body = await res . json ( ) ;
254
- return body ;
255
- }
256
- catch ( err ) {
257
- throw new Error ( "Specref returned invalid JSON" ) ;
258
- }
250
+ return fetchJSON ( specrefUrl , options ) ;
259
251
} ) ) ;
260
252
261
253
const results = { } ;
@@ -315,54 +307,17 @@ async function fetchInfoFromSpecref(specs, options) {
315
307
316
308
317
309
async function fetchInfoFromIETF ( specs , options ) {
318
- async function fetchJSONDoc ( draftName ) {
319
- const url = `https://datatracker.ietf.org/doc/${ draftName } /doc.json` ;
320
- const res = await fetchQueue . runThrottled ( fetch , url , options ) ;
321
- if ( res . status !== 200 ) {
322
- throw new Error ( `IETF datatracker returned an error for ${ url } , status code is ${ res . status } ` ) ;
323
- }
324
- try {
325
- return await res . json ( ) ;
326
- }
327
- catch ( err ) {
328
- throw new Error ( `IETF datatracker returned invalid JSON for ${ url } ` ) ;
329
- }
330
- }
331
-
332
310
async function fetchRFCName ( docUrl ) {
333
- const res = await fetchQueue . runThrottled ( fetch , docUrl , options ) ;
334
- if ( res . status !== 200 ) {
335
- throw new Error ( `IETF datatracker returned an error for ${ url } , status code is ${ res . status } ` ) ;
336
- }
337
- try {
338
- const body = await res . json ( ) ;
339
- if ( ! body . rfc ) {
340
- throw new Error ( `Could not find an RFC name in ${ docUrl } ` ) ;
341
- }
342
- return `rfc${ body . rfc } ` ;
343
- }
344
- catch ( err ) {
345
- throw new Error ( `IETF datatracker returned invalid JSON for ${ url } ` ) ;
346
- }
311
+ const body = await fetchJSON ( docUrl , options ) ;
312
+ return `rfc${ body . rfc } ` ;
347
313
}
348
314
349
315
async function fetchObsoletedBy ( draftName ) {
350
316
if ( ! draftName . startsWith ( 'rfc' ) ) {
351
317
return [ ] ;
352
318
}
353
319
const url = `https://datatracker.ietf.org/api/v1/doc/relateddocument/?format=json&relationship__slug__in=obs&target__name__in=${ draftName } ` ;
354
- const res = await fetchQueue . runThrottled ( fetch , url , options ) ;
355
- if ( res . status !== 200 ) {
356
- throw new Error ( `IETF datatracker returned an error for ${ url } , status code is ${ res . status } ` ) ;
357
- }
358
- let body ;
359
- try {
360
- body = await res . json ( ) ;
361
- }
362
- catch ( err ) {
363
- throw new Error ( `IETF datatracker returned invalid JSON for ${ url } ` ) ;
364
- }
365
-
320
+ const body = await fetchJSON ( url , options ) ;
366
321
return Promise . all ( body . objects
367
322
. map ( obj => `https://datatracker.ietf.org${ obj . source } ` )
368
323
. map ( fetchRFCName ) ) ;
@@ -388,6 +343,15 @@ async function fetchInfoFromIETF(specs, options) {
388
343
return paths . filter ( p => p . path . match ( / ^ s p e c s \/ r f c \d + \. h t m l $ / ) )
389
344
. map ( p => p . path . match ( / ( r f c \d + ) \. h t m l $ / ) [ 1 ] ) ;
390
345
}
346
+
347
+ // IETF can only provide information about IETF specs, no need to fetch the
348
+ // list of RFCs of the HTTP WG if there's no IETF spec in the list.
349
+ if ( ! specs . find ( spec =>
350
+ spec . url . match ( / \. r f c - e d i t o r \. o r g / ) ||
351
+ spec . url . match ( / d a t a t r a c k e r \. i e t f \. o r g / ) ) ) {
352
+ return { } ;
353
+ }
354
+
391
355
const httpwgRFCs = await getHttpwgRFCs ( ) ;
392
356
393
357
const info = await Promise . all ( specs . map ( async spec => {
@@ -404,7 +368,8 @@ async function fetchInfoFromIETF(specs, options) {
404
368
if ( ! draftName ) {
405
369
throw new Error ( `IETF document follows an unexpected URL pattern: ${ spec . url } ` ) ;
406
370
}
407
- const jsonDoc = await fetchJSONDoc ( draftName [ 1 ] ) ;
371
+ const draftUrl = `https://datatracker.ietf.org/doc/${ draftName [ 1 ] } /doc.json` ;
372
+ const jsonDoc = await fetchJSON ( draftUrl , options ) ;
408
373
const lastRevision = jsonDoc . rev_history . pop ( ) ;
409
374
if ( lastRevision . name !== draftName [ 1 ] ) {
410
375
throw new Error ( `IETF spec ${ spec . url } published under a new name "${ lastRevision . name } ". Canonical URL must be updated accordingly.` ) ;
@@ -645,13 +610,16 @@ async function fetchInfo(specs, options) {
645
610
{ name : 'discontinued' , fn : useLastInfoForDiscontinuedSpecs } ,
646
611
{ name : 'w3c' , fn : fetchInfoFromW3CApi } ,
647
612
{ name : 'ietf' , fn : fetchInfoFromIETF } ,
613
+ { name : 'whatwg' , fn : fetchInfoFromWHATWG } ,
648
614
{ name : 'specref' , fn : fetchInfoFromSpecref } ,
649
615
{ name : 'spec' , fn : fetchInfoFromSpecs }
650
616
] ;
651
617
let remainingSpecs = specs ;
652
618
for ( let i = 0 ; i < steps . length ; i ++ ) {
653
619
const step = steps [ i ] ;
654
- info [ step . name ] = await step . fn ( remainingSpecs , options ) ;
620
+ info [ step . name ] = remainingSpecs . length > 0 ?
621
+ await step . fn ( remainingSpecs , options ) :
622
+ { } ;
655
623
remainingSpecs = remainingSpecs . filter ( spec => ! info [ step . name ] [ spec . shortname ] ) ;
656
624
}
657
625
0 commit comments