reffy 5.2.3 → 6.1.2
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/README.md +4 -0
- package/index.js +5 -1
- package/package.json +8 -8
- package/src/browserlib/extract-webidl.mjs +12 -3
- package/src/cli/check-missing-dfns.js +1 -1
- package/src/cli/generate-idlnames.js +7 -18
- package/src/cli/generate-idlparsed.js +139 -0
- package/src/cli/parse-webidl.js +11 -3
- package/src/lib/nock-server.js +40 -23
- package/src/lib/specs-crawler.js +18 -41
- package/src/lib/util.js +83 -42
package/README.md
CHANGED
|
@@ -93,6 +93,10 @@ To create the WebIDL extract in the first place, you will need to run the `idl`
|
|
|
93
93
|
reffy --spec fetch --module idl > fetch.idl
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
### Parsed WebIDL generator
|
|
97
|
+
|
|
98
|
+
The **Parsed WebIDL generator** takes the results of a crawl as input and applies the WebIDL parser to all specs it contains to create JSON extracts in an `idlparsed` folder. To run the generator: `node src/cli/generate-idlparsed.js [crawl folder] [save folder]`
|
|
99
|
+
|
|
96
100
|
|
|
97
101
|
### WebIDL names generator
|
|
98
102
|
|
package/index.js
CHANGED
|
@@ -3,5 +3,9 @@ module.exports = {
|
|
|
3
3
|
crawlSpecs: require("./src/lib/specs-crawler").crawlList,
|
|
4
4
|
expandCrawlResult: require("./src/lib/util").expandCrawlResult,
|
|
5
5
|
mergeCrawlResults: require("./src/lib/util").mergeCrawlResults,
|
|
6
|
-
isLatestLevelThatPasses: require("./src/lib/util").isLatestLevelThatPasses
|
|
6
|
+
isLatestLevelThatPasses: require("./src/lib/util").isLatestLevelThatPasses,
|
|
7
|
+
generateIdlNames: require("./src/cli/generate-idlnames").generateIdlNames,
|
|
8
|
+
saveIdlNames: require("./src/cli/generate-idlnames").saveIdlNames,
|
|
9
|
+
generateIdlParsed: require("./src/cli/generate-idlparsed").generateIdlParsed,
|
|
10
|
+
saveIdlParsed: require("./src/cli/generate-idlparsed").saveIdlParsed
|
|
7
11
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reffy",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.1.2",
|
|
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",
|
|
@@ -32,20 +32,20 @@
|
|
|
32
32
|
"bin": "./reffy.js",
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"abortcontroller-polyfill": "1.7.3",
|
|
35
|
-
"browser-specs": "2.
|
|
35
|
+
"browser-specs": "2.22.0",
|
|
36
36
|
"commander": "8.3.0",
|
|
37
37
|
"fetch-filecache-for-crawling": "4.0.2",
|
|
38
|
-
"puppeteer": "13.
|
|
38
|
+
"puppeteer": "13.1.2",
|
|
39
39
|
"semver": "^7.3.5",
|
|
40
40
|
"webidl2": "24.2.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"chai": "4.3.
|
|
44
|
-
"mocha": "9.
|
|
45
|
-
"nock": "13.2.
|
|
46
|
-
"respec": "28.
|
|
43
|
+
"chai": "4.3.5",
|
|
44
|
+
"mocha": "9.2.0",
|
|
45
|
+
"nock": "13.2.2",
|
|
46
|
+
"respec": "28.2.6",
|
|
47
47
|
"respec-hljs": "2.1.1",
|
|
48
|
-
"rollup": "2.
|
|
48
|
+
"rollup": "2.66.1"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"test": "mocha --recursive tests/"
|
|
@@ -11,19 +11,28 @@ import informativeSelector from './informative-selector.mjs';
|
|
|
11
11
|
*/
|
|
12
12
|
export default function () {
|
|
13
13
|
const generator = getGenerator();
|
|
14
|
+
let idl = '';
|
|
14
15
|
if (generator === 'bikeshed') {
|
|
15
|
-
|
|
16
|
+
idl = extractBikeshedIdl();
|
|
16
17
|
}
|
|
17
18
|
else if (document.title.startsWith('Web IDL')) {
|
|
18
19
|
// IDL content in the Web IDL spec are... examples,
|
|
19
20
|
// not real definitions
|
|
20
|
-
return '';
|
|
21
21
|
}
|
|
22
22
|
else {
|
|
23
23
|
// Most non-ReSpec specs still follow the ReSpec conventions
|
|
24
24
|
// for IDL definitions
|
|
25
|
-
|
|
25
|
+
idl = extractRespecIdl();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (idl) {
|
|
29
|
+
// Remove trailing spaces and use spaces throughout
|
|
30
|
+
idl = idl
|
|
31
|
+
.replace(/\s+$/gm, '\n')
|
|
32
|
+
.replace(/\t/g, ' ')
|
|
33
|
+
.trim();
|
|
26
34
|
}
|
|
35
|
+
return idl;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
|
|
@@ -399,7 +399,7 @@ function checkSpecDefinitions(spec, options = {}) {
|
|
|
399
399
|
(spec.css || {});
|
|
400
400
|
const idl = (typeof spec.idlparsed === "string") ?
|
|
401
401
|
require(path.resolve(options.rootFolder, spec.idlparsed)).idlparsed :
|
|
402
|
-
spec.
|
|
402
|
+
spec.idlparsed;
|
|
403
403
|
|
|
404
404
|
// Make sure that all expected CSS definitions exist in the dfns extract
|
|
405
405
|
const expectedCSSDfns = getExpectedDfnsFromCSS(css);
|
|
@@ -28,7 +28,8 @@ const { matchIdlDfn, getExpectedDfnFromIdlDesc } = require('./check-missing-dfns
|
|
|
28
28
|
const {
|
|
29
29
|
expandCrawlResult,
|
|
30
30
|
isLatestLevelThatPasses,
|
|
31
|
-
requireFromWorkingDirectory
|
|
31
|
+
requireFromWorkingDirectory,
|
|
32
|
+
createFolderIfNeeded
|
|
32
33
|
} = require('../lib/util');
|
|
33
34
|
|
|
34
35
|
|
|
@@ -118,7 +119,7 @@ function generateIdlNames(results, options = {}) {
|
|
|
118
119
|
const names = {};
|
|
119
120
|
|
|
120
121
|
function defineIDLContent(spec) {
|
|
121
|
-
return spec.
|
|
122
|
+
return spec.idlparsed?.idlNames || spec.idlparsed?.idlExtendedNames;
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
// Only keep latest version of specs and delta specs that define some IDL
|
|
@@ -129,10 +130,10 @@ function generateIdlNames(results, options = {}) {
|
|
|
129
130
|
// Add main definitions of all IDL names
|
|
130
131
|
// (using the latest version of a spec that defines some IDL)
|
|
131
132
|
results.forEach(spec => {
|
|
132
|
-
if (!spec.
|
|
133
|
+
if (!spec.idlparsed.idlNames) {
|
|
133
134
|
return;
|
|
134
135
|
}
|
|
135
|
-
Object.entries(spec.
|
|
136
|
+
Object.entries(spec.idlparsed.idlNames).forEach(([name, idl]) => {
|
|
136
137
|
const desc = Object.assign(specInfo(spec), { fragment: idl.fragment });
|
|
137
138
|
fragments[idl.fragment] = idl;
|
|
138
139
|
|
|
@@ -157,10 +158,10 @@ function generateIdlNames(results, options = {}) {
|
|
|
157
158
|
|
|
158
159
|
// Add definitions that extend base definitions
|
|
159
160
|
results.forEach(spec => {
|
|
160
|
-
if (!spec.
|
|
161
|
+
if (!spec.idlparsed.idlExtendedNames) {
|
|
161
162
|
return;
|
|
162
163
|
}
|
|
163
|
-
Object.entries(spec.
|
|
164
|
+
Object.entries(spec.idlparsed.idlExtendedNames).forEach(([name, extensions]) =>
|
|
164
165
|
extensions.forEach(idl => {
|
|
165
166
|
const desc = Object.assign(specInfo(spec), { fragment: idl.fragment });
|
|
166
167
|
fragments[idl.fragment] = idl;
|
|
@@ -308,18 +309,6 @@ async function generateIdlNamesFromPath(crawlPath, options = {}) {
|
|
|
308
309
|
}
|
|
309
310
|
|
|
310
311
|
|
|
311
|
-
async function createFolderIfNeeded(name) {
|
|
312
|
-
try {
|
|
313
|
-
await fs.promises.mkdir(name);
|
|
314
|
-
}
|
|
315
|
-
catch (err) {
|
|
316
|
-
if (err.code !== 'EEXIST') {
|
|
317
|
-
throw err;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
312
|
/**
|
|
324
313
|
* Save IDL names to individual JSON files in the given folder
|
|
325
314
|
*
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The parsed IDL generator takes a crawl report or a single spec as input, and
|
|
4
|
+
* generates (or re-generates if it already exists) a parsed IDL structure from
|
|
5
|
+
* the raw IDL that the spec defines. Result is dumped to the console or saved
|
|
6
|
+
* to the given folder.
|
|
7
|
+
*
|
|
8
|
+
* The parsed IDL generator is used by the crawler to create and save the parsed
|
|
9
|
+
* IDL structures. It is also useful to re-generated the parsed IDL info when
|
|
10
|
+
* an IDL patch has been applied to the raw IDL.
|
|
11
|
+
*
|
|
12
|
+
* The parsed IDL generator can be called directly through:
|
|
13
|
+
*
|
|
14
|
+
* `node generate-idlparsed.js [crawl report] [save folder]`
|
|
15
|
+
*
|
|
16
|
+
* where `crawl report` is the path to the folder that contains the
|
|
17
|
+
* `index.json` file and all other crawl results produced by specs-crawler.js,
|
|
18
|
+
* and `save folder` is an optional folder (which must exist) where IDL
|
|
19
|
+
* name extracts are to be saved. In the absence of this parameter, the report
|
|
20
|
+
* is written to the console.
|
|
21
|
+
*
|
|
22
|
+
* When a folder is provided, the IDL name extracts are saved as a JSON
|
|
23
|
+
* structure in an `idlparsed` subfolder.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const webidlParser = require('../cli/parse-webidl');
|
|
29
|
+
const {
|
|
30
|
+
expandCrawlResult,
|
|
31
|
+
requireFromWorkingDirectory,
|
|
32
|
+
createFolderIfNeeded
|
|
33
|
+
} = require('../lib/util');
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Update the spec object in place with parsed IDL information.
|
|
38
|
+
*
|
|
39
|
+
* @function
|
|
40
|
+
* @public
|
|
41
|
+
* @param {Object} spec The spec object to update. The function looks for the
|
|
42
|
+
* raw IDL in the `idl` property.
|
|
43
|
+
* @return {Object} The updated spec with an `idl` property that contains the
|
|
44
|
+
* parsed version of the IDL, and the raw IDL moved under the `idl.idl`
|
|
45
|
+
* sub-property. Note the spec object is updated in place.
|
|
46
|
+
*/
|
|
47
|
+
async function generateIdlParsed(spec) {
|
|
48
|
+
if (!spec?.idl) {
|
|
49
|
+
return spec;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
spec.idlparsed = await webidlParser.parse(spec.idl);
|
|
53
|
+
spec.idlparsed.hasObsoleteIdl = webidlParser.hasObsoleteIdl(spec.idl);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// IDL content is invalid and cannot be parsed.
|
|
57
|
+
// Let's return the error, along with the raw IDL
|
|
58
|
+
// content so that it may be saved to a file.
|
|
59
|
+
spec.idlparsed = err.toString();
|
|
60
|
+
}
|
|
61
|
+
return spec;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async function generateIdlParsedFromPath(crawlPath) {
|
|
66
|
+
const crawlIndex = requireFromWorkingDirectory(path.resolve(crawlPath, 'index.json'));
|
|
67
|
+
const crawlResults = await expandCrawlResult(crawlIndex, crawlPath, ['idl']);
|
|
68
|
+
await Promise.all(crawlResults.results.map(generateIdlParsed));
|
|
69
|
+
return crawlResults;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate the `idlparsed` export for the spec.
|
|
75
|
+
*
|
|
76
|
+
* Note that the raw IDL (under `spec.idl.idl`) gets deleted in the process.
|
|
77
|
+
*
|
|
78
|
+
* @function
|
|
79
|
+
* @public
|
|
80
|
+
* @param {Object} spec Spec object with the parsed IDL
|
|
81
|
+
* @param {String} folder Path to root folder where `idlparsed` folder needs to
|
|
82
|
+
* appear.
|
|
83
|
+
* @return {String} The relative path from the root folder to the generated file
|
|
84
|
+
*/
|
|
85
|
+
async function saveIdlParsed(spec, folder) {
|
|
86
|
+
function specInfo(spec) {
|
|
87
|
+
return {
|
|
88
|
+
spec: {
|
|
89
|
+
title: spec.title,
|
|
90
|
+
url: spec.crawled
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const subfolder = path.join(folder, 'idlparsed');
|
|
96
|
+
await createFolderIfNeeded(subfolder);
|
|
97
|
+
|
|
98
|
+
if (!spec?.idlparsed) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const json = JSON.stringify(
|
|
103
|
+
Object.assign(specInfo(spec), { idlparsed: spec.idlparsed }),
|
|
104
|
+
null, 2);
|
|
105
|
+
const filename = path.join(subfolder, spec.shortname + '.json');
|
|
106
|
+
await fs.promises.writeFile(filename, json);
|
|
107
|
+
return `idlparsed/${spec.shortname}.json`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
/**************************************************
|
|
112
|
+
Export methods for use as module
|
|
113
|
+
**************************************************/
|
|
114
|
+
module.exports.generateIdlParsed = generateIdlParsed;
|
|
115
|
+
module.exports.saveIdlParsed = saveIdlParsed;
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
/**************************************************
|
|
119
|
+
Code run if the code is run as a stand-alone module
|
|
120
|
+
**************************************************/
|
|
121
|
+
if (require.main === module) {
|
|
122
|
+
const crawlPath = process.argv[2];
|
|
123
|
+
if (!crawlPath) {
|
|
124
|
+
console.error('Required path to crawl results folder is missing');
|
|
125
|
+
process.exit(2);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const savePath = process.argv[3];
|
|
129
|
+
generateIdlParsedFromPath(crawlPath)
|
|
130
|
+
.then(report => {
|
|
131
|
+
if (savePath) {
|
|
132
|
+
return Promise.all(report.results.map(
|
|
133
|
+
spec => saveIdlParsed(spec, savePath)));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.log(JSON.stringify(report, null, 2));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
package/src/cli/parse-webidl.js
CHANGED
|
@@ -261,8 +261,14 @@ function parseInterfaceOrDictionary(def, idlReport) {
|
|
|
261
261
|
|
|
262
262
|
const exposedEA = def.extAttrs.find(ea => ea.name === "Exposed");
|
|
263
263
|
if (exposedEA && exposedEA.rhs) {
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
let exposedNames = [];
|
|
265
|
+
if (exposedEA.rhs.type === "*") {
|
|
266
|
+
exposedNames.push("*");
|
|
267
|
+
} else if (exposedEA.rhs.type === "identifier") {
|
|
268
|
+
exposedNames.push(exposedEA.rhs.value);
|
|
269
|
+
} else {
|
|
270
|
+
exposedNames = exposedEA.rhs.value.map(c => c.value);
|
|
271
|
+
}
|
|
266
272
|
exposedNames.forEach(name => {
|
|
267
273
|
if (!exposed[name]) {
|
|
268
274
|
exposed[name] = [];
|
|
@@ -313,7 +319,9 @@ function addToJSContext(eas, jsNames, name, type) {
|
|
|
313
319
|
var exposed = eas && eas.some(ea => ea.name === "Exposed");
|
|
314
320
|
if (exposed) {
|
|
315
321
|
var exposedEa = eas.find(ea => ea.name === "Exposed");
|
|
316
|
-
if (exposedEa.rhs.type === "
|
|
322
|
+
if (exposedEa.rhs.type === "*") {
|
|
323
|
+
contexts = ["*"];
|
|
324
|
+
} else if (exposedEa.rhs.type === "identifier") {
|
|
317
325
|
contexts = [exposedEa.rhs.value];
|
|
318
326
|
} else {
|
|
319
327
|
contexts = exposedEa.rhs.value.map(c => c.value);
|
package/src/lib/nock-server.js
CHANGED
|
@@ -9,7 +9,6 @@ const nock = require("nock");
|
|
|
9
9
|
const path = require("path");
|
|
10
10
|
const { existsSync } = require('fs');
|
|
11
11
|
|
|
12
|
-
|
|
13
12
|
/**
|
|
14
13
|
* Determine the path to the "node_modules" folder. The path depends on whether
|
|
15
14
|
* Reffy is run directly, or installed as a library.
|
|
@@ -30,14 +29,29 @@ const modulesFolder = getModulesFolder();
|
|
|
30
29
|
|
|
31
30
|
const mockSpecs = {
|
|
32
31
|
"/woff/woff2/": {
|
|
33
|
-
html:
|
|
32
|
+
html: `
|
|
33
|
+
<title>WOFF2</title>
|
|
34
|
+
<body>
|
|
35
|
+
<dfn id='foo'>Foo</dfn>
|
|
36
|
+
<a href="https://www.w3.org/TR/bar/#baz">bar</a>
|
|
37
|
+
<ul class='toc'><li><a href='page.html'>page</a></ul>`,
|
|
34
38
|
pages: {
|
|
35
39
|
"page.html": `<h2 id='bar'>Heading in subpage</h2>`
|
|
36
40
|
}
|
|
37
41
|
},
|
|
38
|
-
"/mediacapture-output/":
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
"/mediacapture-output/": `
|
|
43
|
+
<script>respecConfig = { shortName: 'test' };</script>
|
|
44
|
+
<script src='https://www.w3.org/Tools/respec/respec-w3c'></script>
|
|
45
|
+
<div id=abstract></div>
|
|
46
|
+
<pre class='idl'>[Exposed=Window] interface Foo { attribute DOMString bar; };</pre>`,
|
|
47
|
+
"/accelerometer/": `<html>
|
|
48
|
+
<h2>Normative references</h2>
|
|
49
|
+
<dl>
|
|
50
|
+
<dt>FOO</dt>
|
|
51
|
+
<dd><a href='https://www.w3.org/TR/Foo'>Foo</a></dd>
|
|
52
|
+
</dl>`,
|
|
53
|
+
"/pointerlock/": `<html>
|
|
54
|
+
<h1>Pointer Lock 2.0`
|
|
41
55
|
};
|
|
42
56
|
|
|
43
57
|
nock.disableNetConnect();
|
|
@@ -48,47 +62,50 @@ Object.keys(mockSpecs).forEach(path => {
|
|
|
48
62
|
nock("https://w3c.github.io")
|
|
49
63
|
.persist()
|
|
50
64
|
.get(path)
|
|
51
|
-
.reply(200,
|
|
65
|
+
.reply(200,
|
|
66
|
+
typeof mockSpecs[path] === "string" ? mockSpecs[path] : mockSpecs[path].html,
|
|
67
|
+
{ 'Content-Type': 'text/html' }
|
|
68
|
+
);
|
|
52
69
|
|
|
53
70
|
Object.keys(mockSpecs[path].pages || {}).forEach(page => {
|
|
54
71
|
nock("https://w3c.github.io")
|
|
55
72
|
.persist()
|
|
56
73
|
.get(path + page)
|
|
57
|
-
.reply(200,
|
|
74
|
+
.reply(200,
|
|
75
|
+
mockSpecs[path].pages[page],
|
|
76
|
+
{ 'Content-Type': 'text/html' });
|
|
58
77
|
|
|
59
78
|
});
|
|
60
79
|
});
|
|
61
80
|
|
|
62
81
|
|
|
63
|
-
// Handling requests generated by ReSpec
|
|
64
|
-
nock("https://respec.org")
|
|
65
|
-
.persist()
|
|
66
|
-
.options("/xref/").reply(204, '', {"Access-Control-Allow-Methods": "POST,GET",
|
|
67
|
-
"Access-Control-Allow-Origin": "*"}).
|
|
68
|
-
post("/xref/").reply(200, {"result":[["cc15613180c92a877452c092012792b9572ad189",[{"shortname":"webidl","spec":"webidl","type":"extended-attribute","normative":true,"uri":"#Exposed"}]],["a28dcf4738f5492eb05f1fd8a27b8ce0ae124d21",[{"shortname":"webidl","spec":"webidl","type":"interface","normative":true,"uri":"#idl-DOMString"}]],["2eb09984ad7f314b43fefeb75a6feedb049ad595",[]]]});
|
|
69
|
-
|
|
82
|
+
// Handling requests generated by ReSpec documents
|
|
70
83
|
nock("https://api.specref.org")
|
|
71
84
|
.persist()
|
|
72
|
-
.get("/bibrefs?refs=webidl,html").reply(200,
|
|
73
|
-
|
|
85
|
+
.get("/bibrefs?refs=webidl,html").reply(200,
|
|
86
|
+
{ webidl: { href: "https://webidl.spec.whatwg.org/" } },
|
|
87
|
+
{ "Access-Control-Allow-Origin": "*" }
|
|
88
|
+
);
|
|
74
89
|
|
|
75
90
|
nock("https://www.w3.org")
|
|
76
91
|
.persist()
|
|
77
92
|
.get("/scripts/TR/2021/fixup.js").reply(200, '')
|
|
78
93
|
.get("/StyleSheets/TR/2021/logos/W3C").reply(200, '')
|
|
79
94
|
.get("/StyleSheets/TR/2021/base.css").reply(200, '')
|
|
80
|
-
.get("/Tools/respec/respec-highlight").replyWithFile(200,
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
.get("/Tools/respec/respec-highlight").replyWithFile(200,
|
|
96
|
+
path.join(modulesFolder, "respec-hljs", "dist", "respec-highlight.js"),
|
|
97
|
+
{ "Content-Type": "application/js" })
|
|
98
|
+
.get("/Tools/respec/respec-w3c").replyWithFile(200,
|
|
99
|
+
path.join(modulesFolder, "respec", "builds", "respec-w3c.js"),
|
|
100
|
+
{ "Content-Type": "application/js" });
|
|
83
101
|
|
|
84
|
-
nock.emitter.on('error', function(err) {
|
|
85
|
-
|
|
102
|
+
nock.emitter.on('error', function (err) {
|
|
103
|
+
console.error(err);
|
|
86
104
|
});
|
|
87
105
|
nock.emitter.on('no match', function(req, options, requestBody) {
|
|
88
106
|
// 127.0.0.1 is used by the devtool protocol, we ignore it
|
|
89
107
|
if (req && req.hostname !== '127.0.0.1') {
|
|
90
|
-
|
|
91
|
-
throw(error);
|
|
108
|
+
console.error("No match for nock request on " + (options ? options.href : req.href));
|
|
92
109
|
}
|
|
93
110
|
});
|
|
94
111
|
|
package/src/lib/specs-crawler.js
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const specs = require('browser-specs');
|
|
16
|
-
const webidlParser = require('../cli/parse-webidl');
|
|
17
16
|
const cssDfnParser = require('./css-grammar-parser');
|
|
17
|
+
const { generateIdlParsed, saveIdlParsed } = require('../cli/generate-idlparsed');
|
|
18
18
|
const { generateIdlNames, saveIdlNames } = require('../cli/generate-idlnames');
|
|
19
19
|
const {
|
|
20
20
|
completeWithAlternativeUrls,
|
|
@@ -24,7 +24,8 @@ const {
|
|
|
24
24
|
isLatestLevelThatPasses,
|
|
25
25
|
processSpecification,
|
|
26
26
|
setupBrowser,
|
|
27
|
-
teardownBrowser
|
|
27
|
+
teardownBrowser,
|
|
28
|
+
createFolderIfNeeded
|
|
28
29
|
} = require('./util');
|
|
29
30
|
|
|
30
31
|
|
|
@@ -62,26 +63,13 @@ async function crawlSpec(spec, crawlOptions) {
|
|
|
62
63
|
return res;
|
|
63
64
|
},
|
|
64
65
|
[spec, crawlOptions.modules],
|
|
65
|
-
{ quiet: crawlOptions.quiet
|
|
66
|
+
{ quiet: crawlOptions.quiet,
|
|
67
|
+
forceLocalFetch: crawlOptions.forceLocalFetch }
|
|
66
68
|
);
|
|
67
69
|
|
|
68
70
|
// Specific rule for IDL extracts:
|
|
69
71
|
// parse the extracted WebIdl content
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const parsedIdl = await webidlParser.parse(result.idl);
|
|
73
|
-
parsedIdl.hasObsoleteIdl = webidlParser.hasObsoleteIdl(result.idl);
|
|
74
|
-
parsedIdl.idl = result.idl;
|
|
75
|
-
result.idl = parsedIdl;
|
|
76
|
-
}
|
|
77
|
-
catch (err) {
|
|
78
|
-
// IDL content is invalid and cannot be parsed.
|
|
79
|
-
// Let's return the error, along with the raw IDL
|
|
80
|
-
// content so that it may be saved to a file.
|
|
81
|
-
err.idl = result.idl;
|
|
82
|
-
result.idl = err;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
72
|
+
await generateIdlParsed(result);
|
|
85
73
|
|
|
86
74
|
if (result.css) {
|
|
87
75
|
// Specific rule for CSS properties:
|
|
@@ -151,6 +139,9 @@ async function crawlSpec(spec, crawlOptions) {
|
|
|
151
139
|
crawlOptions.modules.forEach(mod => {
|
|
152
140
|
if (result[mod.property]) {
|
|
153
141
|
spec[mod.property] = result[mod.property];
|
|
142
|
+
if (mod.property === 'idl') {
|
|
143
|
+
spec.idlparsed = result.idlparsed;
|
|
144
|
+
}
|
|
154
145
|
}
|
|
155
146
|
});
|
|
156
147
|
}
|
|
@@ -186,14 +177,7 @@ async function saveSpecResults(spec, settings) {
|
|
|
186
177
|
|
|
187
178
|
async function getSubfolder(name) {
|
|
188
179
|
let subfolder = path.join(settings.output, name);
|
|
189
|
-
|
|
190
|
-
await fs.promises.mkdir(subfolder);
|
|
191
|
-
}
|
|
192
|
-
catch (err) {
|
|
193
|
-
if (err.code !== 'EEXIST') {
|
|
194
|
-
throw err;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
180
|
+
await createFolderIfNeeded(subfolder);
|
|
197
181
|
return subfolder;
|
|
198
182
|
}
|
|
199
183
|
|
|
@@ -242,13 +226,10 @@ async function saveSpecResults(spec, settings) {
|
|
|
242
226
|
// (https://github.com/w3c/webref)
|
|
243
227
|
// Source: ${spec.title} (${spec.crawled})`;
|
|
244
228
|
idlHeader = idlHeader.replace(/^\s+/gm, '').trim() + '\n\n';
|
|
245
|
-
|
|
246
|
-
.replace(/\s+$/gm, '\n')
|
|
247
|
-
.replace(/\t/g, ' ')
|
|
248
|
-
.trim();
|
|
249
|
-
idl = idlHeader + idl + '\n';
|
|
229
|
+
const idl = idlHeader + spec.idl + '\n';
|
|
250
230
|
await fs.promises.writeFile(
|
|
251
231
|
path.join(folders.idl, spec.shortname + '.idl'), idl);
|
|
232
|
+
return `idl/${spec.shortname}.idl`;
|
|
252
233
|
};
|
|
253
234
|
|
|
254
235
|
async function saveCss(spec) {
|
|
@@ -265,19 +246,15 @@ async function saveSpecResults(spec, settings) {
|
|
|
265
246
|
}, 2) + '\n';
|
|
266
247
|
const pathname = path.join(folders.css, spec.shortname + '.json')
|
|
267
248
|
await fs.promises.writeFile(pathname, json);
|
|
268
|
-
|
|
249
|
+
return `css/${spec.shortname}.json`;
|
|
269
250
|
};
|
|
270
251
|
|
|
271
252
|
// Save IDL dumps
|
|
272
|
-
if (spec.idl
|
|
273
|
-
await saveIdl(spec);
|
|
274
|
-
delete spec.idl.idl;
|
|
275
|
-
spec.idlparsed = spec.idl;
|
|
276
|
-
spec.idl = `idl/${spec.shortname}.idl`;
|
|
277
|
-
await saveExtract(spec, 'idlparsed', spec => spec.idlparsed);
|
|
253
|
+
if (spec.idl) {
|
|
254
|
+
spec.idl = await saveIdl(spec);
|
|
278
255
|
}
|
|
279
|
-
|
|
280
|
-
|
|
256
|
+
if (spec.idlparsed) {
|
|
257
|
+
spec.idlparsed = await saveIdlParsed(spec, settings.output);
|
|
281
258
|
}
|
|
282
259
|
|
|
283
260
|
// Save CSS dumps
|
|
@@ -288,7 +265,7 @@ async function saveSpecResults(spec, settings) {
|
|
|
288
265
|
(Object.keys(spec.css.valuespaces || {}).length > 0));
|
|
289
266
|
}
|
|
290
267
|
if (defineCSSContent(spec)) {
|
|
291
|
-
await saveCss(spec);
|
|
268
|
+
spec.css = await saveCss(spec);
|
|
292
269
|
}
|
|
293
270
|
|
|
294
271
|
// Specs that define CSS now have a "css" key that point to the CSS extract.
|
package/src/lib/util.js
CHANGED
|
@@ -83,6 +83,14 @@ const modulesFolder = getModulesFolder();
|
|
|
83
83
|
*/
|
|
84
84
|
let browser = null;
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Promise resolved when there is no running instance of Puppeteer. This allows
|
|
88
|
+
* to serialize calls to setupBrowser (and thus to crawlList and crawlSpecs in
|
|
89
|
+
* specs-crawler.js)
|
|
90
|
+
*/
|
|
91
|
+
let browserClosed = Promise.resolve();
|
|
92
|
+
let resolveBrowserClosed = null;
|
|
93
|
+
|
|
86
94
|
/**
|
|
87
95
|
* The browser JS library that will be loaded onto every crawled page
|
|
88
96
|
*/
|
|
@@ -228,9 +236,13 @@ window.reffy.${module.name} = ${module.name};
|
|
|
228
236
|
* @public
|
|
229
237
|
*/
|
|
230
238
|
async function setupBrowser(modules) {
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
239
|
+
// There can be only one crawl running at a time
|
|
240
|
+
await browserClosed;
|
|
241
|
+
browserClosed = new Promise(resolve => resolveBrowserClosed = resolve);
|
|
242
|
+
|
|
243
|
+
// Create browser instance
|
|
244
|
+
// Note: switch "headless" to "false" (and comment out the call to
|
|
245
|
+
// "browser.close()") to access dev tools in debug mode
|
|
234
246
|
browser = await puppeteer.launch({ headless: true });
|
|
235
247
|
setupBrowserlib(modules);
|
|
236
248
|
}
|
|
@@ -248,6 +260,8 @@ async function teardownBrowser() {
|
|
|
248
260
|
if (browser) {
|
|
249
261
|
await browser.close();
|
|
250
262
|
browser = null;
|
|
263
|
+
resolveBrowserClosed();
|
|
264
|
+
resolveBrowserClosed = null;
|
|
251
265
|
}
|
|
252
266
|
}
|
|
253
267
|
|
|
@@ -306,8 +320,12 @@ async function teardownBrowser() {
|
|
|
306
320
|
* These arguments typically make it possible to pass contextual information
|
|
307
321
|
* to the processing function (such as the spec object that describes the
|
|
308
322
|
* spec being processed, or the list of processing modules to run)
|
|
309
|
-
* @param {Object} options Processing options. The
|
|
310
|
-
*
|
|
323
|
+
* @param {Object} options Processing options. The "quiet" flag tells the
|
|
324
|
+
* function not to report warnings to the console. The "forceLocalFetch"
|
|
325
|
+
* flag tells the function that all network requests need to be only handled
|
|
326
|
+
* by Node.js's "fetch" function (as opposed to falling back to Puppeteer's
|
|
327
|
+
* network and caching logic), which is useful to keep full control of network
|
|
328
|
+
* requests in tests.
|
|
311
329
|
* @return {Promise} The promise to get the results of the processing function
|
|
312
330
|
*/
|
|
313
331
|
async function processSpecification(spec, processFunction, args, options) {
|
|
@@ -416,15 +434,22 @@ async function processSpecification(spec, processFunction, args, options) {
|
|
|
416
434
|
return;
|
|
417
435
|
}
|
|
418
436
|
|
|
419
|
-
// Fetch from file cache failed somehow
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
437
|
+
// Fetch from file cache failed somehow
|
|
438
|
+
// Let Puppeteer handle the request as fallback unless
|
|
439
|
+
// calling function asked us not to do that
|
|
440
|
+
if (options.forceLocalFetch) {
|
|
441
|
+
options.quiet ?? console.warn(`[warn] Network request for ${request.url} failed`, err);
|
|
442
|
+
await cdp.send('Fetch.failRequest', { requestId, errorReason: 'Failed' });
|
|
424
443
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
444
|
+
else {
|
|
445
|
+
options.quiet ?? console.warn(`[warn] Fall back to regular network request for ${request.url}`, err);
|
|
446
|
+
try {
|
|
447
|
+
await cdp.send('Fetch.continueRequest', { requestId });
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
if (!controller.signal.aborted) {
|
|
451
|
+
options.quiet ?? console.warn(`[warn] Fall back to regular network request for ${request.url} failed`, err);
|
|
452
|
+
}
|
|
428
453
|
}
|
|
429
454
|
}
|
|
430
455
|
}
|
|
@@ -434,6 +459,9 @@ async function processSpecification(spec, processFunction, args, options) {
|
|
|
434
459
|
try {
|
|
435
460
|
const page = await browser.newPage();
|
|
436
461
|
|
|
462
|
+
// Disable cache if caller wants to handle all network requests
|
|
463
|
+
await page.setCacheEnabled(!options.forceLocalFetch);
|
|
464
|
+
|
|
437
465
|
// Intercept all network requests to use our own version of "fetch"
|
|
438
466
|
// that makes use of the local file cache.
|
|
439
467
|
const cdp = await page.target().createCDPSession();
|
|
@@ -462,17 +490,17 @@ async function processSpecification(spec, processFunction, args, options) {
|
|
|
462
490
|
// network connections in the past 500ms. This should be enough to
|
|
463
491
|
// handle "redirection" through JS or meta refresh (which would not
|
|
464
492
|
// have time to run if we used "load").
|
|
465
|
-
const
|
|
493
|
+
const loadOptions = {
|
|
466
494
|
timeout: 120000,
|
|
467
495
|
waitUntil: 'networkidle0'
|
|
468
496
|
};
|
|
469
497
|
|
|
470
498
|
// Load the page
|
|
471
499
|
if (spec.html) {
|
|
472
|
-
await page.setContent(spec.html,
|
|
500
|
+
await page.setContent(spec.html, loadOptions);
|
|
473
501
|
}
|
|
474
502
|
else {
|
|
475
|
-
await page.goto(spec.url,
|
|
503
|
+
await page.goto(spec.url, loadOptions);
|
|
476
504
|
}
|
|
477
505
|
|
|
478
506
|
// Handle multi-page specs
|
|
@@ -483,11 +511,12 @@ async function processSpecification(spec, processFunction, args, options) {
|
|
|
483
511
|
for (const url of pageUrls) {
|
|
484
512
|
const subAbort = new AbortController();
|
|
485
513
|
const subPage = await browser.newPage();
|
|
514
|
+
await subPage.setCacheEnabled(!options.forceLocalFetch);
|
|
486
515
|
const subCdp = await subPage.target().createCDPSession();
|
|
487
516
|
await subCdp.send('Fetch.enable');
|
|
488
517
|
subCdp.on('Fetch.requestPaused', interceptRequest(subCdp, subAbort));
|
|
489
518
|
try {
|
|
490
|
-
await subPage.goto(url,
|
|
519
|
+
await subPage.goto(url, loadOptions);
|
|
491
520
|
const html = await subPage.evaluate(() => {
|
|
492
521
|
return document.body.outerHTML
|
|
493
522
|
.replace(/<body/, '<section')
|
|
@@ -688,23 +717,6 @@ async function expandCrawlResult(crawl, baseFolder, properties) {
|
|
|
688
717
|
baseFolder = baseFolder || '';
|
|
689
718
|
|
|
690
719
|
async function expandSpec(spec) {
|
|
691
|
-
// Special case for "idl" that must be processed first
|
|
692
|
-
if (spec.idl && (typeof spec.idl === 'string') &&
|
|
693
|
-
(!properties || properties.includes('idl') || properties.includes('idlparsed'))) {
|
|
694
|
-
if (baseFolder.startsWith('https:')) {
|
|
695
|
-
const url = (new URL(spec.idl, baseFolder)).toString();
|
|
696
|
-
let response = await fetch(url, { nolog: true });
|
|
697
|
-
spec.idl = {
|
|
698
|
-
idl: await response.text()
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
else {
|
|
702
|
-
spec.idl = {
|
|
703
|
-
idl: await fs.readFile(path.join(baseFolder, spec.idl), 'utf8')
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
720
|
await Promise.all(Object.keys(spec).map(async property => {
|
|
709
721
|
// Only consider properties explicitly requested
|
|
710
722
|
if (properties && !properties.includes(property)) {
|
|
@@ -728,6 +740,13 @@ async function expandCrawlResult(crawl, baseFolder, properties) {
|
|
|
728
740
|
const filename = path.join(baseFolder, spec[property]);
|
|
729
741
|
contents = await fs.readFile(filename, 'utf8');
|
|
730
742
|
}
|
|
743
|
+
|
|
744
|
+
// Force UNIX-style line endings
|
|
745
|
+
// (Git may auto-convert LF to CRLF on Windows machines and we
|
|
746
|
+
// want to store multiline IDL fragments as values of properties
|
|
747
|
+
// in parsed IDL trees)
|
|
748
|
+
contents = contents.replace(/\r\n/g, '\n');
|
|
749
|
+
|
|
731
750
|
if (spec[property].endsWith('.json')) {
|
|
732
751
|
contents = JSON.parse(contents);
|
|
733
752
|
}
|
|
@@ -738,14 +757,15 @@ async function expandCrawlResult(crawl, baseFolder, properties) {
|
|
|
738
757
|
delete css.spec;
|
|
739
758
|
spec[property] = css;
|
|
740
759
|
}
|
|
741
|
-
else if (property === '
|
|
742
|
-
// Special case for
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
|
|
760
|
+
else if (property === 'idl') {
|
|
761
|
+
// Special case for raw IDL extracts, which are text extracts.
|
|
762
|
+
// Also drop header that may have been added when extract was
|
|
763
|
+
// serialized.
|
|
764
|
+
if (contents.startsWith('// GENERATED CONTENT - DO NOT EDIT')) {
|
|
765
|
+
const endOfHeader = contents.indexOf('\n\n');
|
|
766
|
+
contents = contents.substring(endOfHeader + 2);
|
|
746
767
|
}
|
|
747
|
-
|
|
748
|
-
delete spec.idlparsed;
|
|
768
|
+
spec.idl = contents;
|
|
749
769
|
}
|
|
750
770
|
else {
|
|
751
771
|
spec[property] = contents[property];
|
|
@@ -810,6 +830,26 @@ function getGeneratedIDLNamesByCSSProperty(property) {
|
|
|
810
830
|
};
|
|
811
831
|
|
|
812
832
|
|
|
833
|
+
/**
|
|
834
|
+
* Creates the given folder if it does not exist yet.
|
|
835
|
+
*
|
|
836
|
+
* @function
|
|
837
|
+
* @public
|
|
838
|
+
* @param {String} folder Path to folder to create
|
|
839
|
+
* (from current working directory)
|
|
840
|
+
*/
|
|
841
|
+
async function createFolderIfNeeded(folder) {
|
|
842
|
+
try {
|
|
843
|
+
await fs.mkdir(folder);
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
if (err.code !== 'EEXIST') {
|
|
847
|
+
throw err;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
|
|
813
853
|
module.exports = {
|
|
814
854
|
fetch,
|
|
815
855
|
requireFromWorkingDirectory,
|
|
@@ -820,5 +860,6 @@ module.exports = {
|
|
|
820
860
|
completeWithAlternativeUrls,
|
|
821
861
|
isLatestLevelThatPasses,
|
|
822
862
|
expandCrawlResult,
|
|
823
|
-
getGeneratedIDLNamesByCSSProperty
|
|
863
|
+
getGeneratedIDLNamesByCSSProperty,
|
|
864
|
+
createFolderIfNeeded
|
|
824
865
|
};
|