html-snapshots 0.18.3 → 1.0.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/.eslintrc.json +4 -5
- package/.github/workflows/verify.yml +2 -2
- package/HISTORY.md +42 -0
- package/README.md +109 -256
- package/examples/README.md +149 -0
- package/examples/custom/myFilter.js +2 -2
- package/examples/custom/package-lock.json +2021 -5
- package/examples/custom/package.json +1 -1
- package/examples/custom/snapshot.js +8 -10
- package/examples/debug-phantomjs/package-lock.json +6 -6
- package/examples/debug-phantomjs/package.json +1 -1
- package/examples/debug-phantomjs/snapshot.js +13 -14
- package/examples/debug-puppeteer/README.md +18 -0
- package/examples/debug-puppeteer/package-lock.json +2804 -0
- package/examples/debug-puppeteer/package.json +12 -0
- package/examples/debug-puppeteer/snapshot.js +30 -0
- package/examples/html5rocks/package-lock.json +1306 -5
- package/examples/html5rocks/package.json +1 -1
- package/examples/html5rocks/snapshot.js +14 -23
- package/examples/process-limit/package-lock.json +1306 -5
- package/examples/process-limit/package.json +1 -1
- package/examples/process-limit/snapshot.js +8 -10
- package/examples/simple-promise/package-lock.json +1306 -5
- package/examples/simple-promise/package.json +1 -1
- package/examples/simple-promise/snapshot.js +6 -8
- package/examples/sitemap-index/package-lock.json +1306 -5
- package/examples/sitemap-index/package.json +1 -1
- package/examples/sitemap-index/snapshot.js +6 -6
- package/examples/utils/index.js +2 -4
- package/examples/verbose/package-lock.json +1306 -5
- package/examples/verbose/package.json +1 -1
- package/examples/verbose/snapshot.js +12 -14
- package/lib/async/index.js +0 -1
- package/lib/common/index.js +1 -1
- package/lib/common/sitemap/index.js +16 -0
- package/lib/common/sitemap/sitemap-collection.js +94 -0
- package/lib/common/sitemap/sitemap-index.js +68 -0
- package/lib/common/{sitemap.js → sitemap/sitemap.js} +68 -57
- package/lib/html-snapshots.js +87 -5
- package/lib/input-generators/_base.js +7 -1
- package/lib/input-generators/array.js +0 -1
- package/lib/input-generators/robots.js +98 -58
- package/lib/input-generators/sitemap-index.js +3 -124
- package/lib/input-generators/sitemap.js +3 -7
- package/lib/input-generators/textfile.js +2 -6
- package/lib/puppeteer/index.js +98 -0
- package/lib/puppeteer/removeScripts.js +8 -0
- package/package.json +9 -4
- package/test/helpers/options.js +4 -2
- package/test/mocha/browsers/puppeteer.js +106 -0
- package/test/mocha/browsers/server/index.html +9 -0
- package/test/mocha/browsers/test.js +11 -0
- package/test/mocha/html-snapshots/basics.js +60 -47
- package/test/mocha/html-snapshots/phantomjs-options.js +54 -60
- package/test/mocha/html-snapshots/process-limit.js +126 -120
- package/test/mocha/html-snapshots/puppeteer.js +47 -0
- package/test/mocha/html-snapshots/robots.js +156 -121
- package/test/mocha/html-snapshots/server/public/page-sitemap.xml +16 -0
- package/test/mocha/html-snapshots/server/test_robots.txt +13 -0
- package/test/mocha/html-snapshots/server/test_robots_sitemap.txt +15 -0
- package/test/mocha/html-snapshots/server/test_robots_sitemap.xml +22 -0
- package/test/mocha/html-snapshots/sitemap-index.js +100 -90
- package/test/mocha/html-snapshots/sitemap.js +75 -31
- package/test/mocha/html-snapshots/snapshot-scripts.js +150 -145
- package/test/mocha/html-snapshots/test.js +23 -14
- package/test/mocha/html-snapshots/test_robots.txt +0 -2
- package/test/mocha/html-snapshots/use-jquery.js +65 -64
- package/test/mocha/html-snapshots/utils.js +29 -30
- package/test/mocha/input-generators/server/test_robots.txt +0 -2
- package/test/mocha/input-generators/server/test_robots_bad.txt +0 -2
- package/test/mocha/input-generators/server/test_robots_sitemap.txt +17 -0
- package/test/mocha/input-generators/server/test_robots_sitemap_bad.txt +17 -0
- package/test/mocha/input-generators/server/test_robots_sitemap_multi.txt +18 -0
- package/test/mocha/input-generators/server/test_sitemap_index_empty.xml +3 -0
- package/test/mocha/input-generators/server/test_sitemap_index_malformed.xml +2 -0
- package/test/mocha/input-generators/sitemap-index.js +65 -9
- package/test/mocha/input-generators/test.js +276 -280
- package/test/mocha/input-generators/test_robots.txt +0 -2
- package/test/mocha/input-generators/test_robots_bad.txt +0 -2
- package/test/mocha/input-generators/test_robots_sitemap.txt +17 -0
- package/test/mocha/input-generators/test_robots_sitemap_bad.txt +17 -0
- package/test/mocha/input-generators/test_robots_sitemap_multi.txt +18 -0
- package/lib/common/node.js +0 -26
- package/test/mocha/common/node.js +0 -100
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* robots.js
|
|
3
3
|
*
|
|
4
4
|
* An input generator for html-snapshots that uses a simple robots.txt file.
|
|
5
|
-
* Creates the snapshot arguments driven from robots.txt
|
|
6
|
-
* Does not support wildcards.
|
|
7
|
-
* If you need wildcards, use the sitemap input generator.
|
|
5
|
+
* Creates the snapshot arguments driven from robots.txt Sitemap or Allow directives.
|
|
8
6
|
*
|
|
9
7
|
* Copyright (c) 2013 - 2022 Alex Grant, LocalNerve, contributors
|
|
10
8
|
* Licensed under the MIT license.
|
|
11
9
|
*/
|
|
12
10
|
|
|
13
|
-
const fs = require("fs");
|
|
14
|
-
const request = require("request");
|
|
11
|
+
const fs = require("fs").promises;
|
|
15
12
|
const common = require("../common");
|
|
16
|
-
const
|
|
13
|
+
const { sitemapCollection: smc } = require("../common/sitemap");
|
|
17
14
|
const base = require("./_base");
|
|
18
15
|
|
|
19
16
|
// The default options
|
|
@@ -39,19 +36,81 @@ const robots = {
|
|
|
39
36
|
};
|
|
40
37
|
|
|
41
38
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
39
|
+
* Get all unique `Allow:` url paths from robots.txt.
|
|
40
|
+
* Reads the allow values up to whitespace (end-of-line), or wildcard.
|
|
41
|
+
*
|
|
42
|
+
* @param {String} body - The robots.txt file
|
|
43
|
+
* @returns {Array} collection of allow url paths
|
|
44
44
|
*/
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
45
|
+
function getAllowUrls (body) {
|
|
46
|
+
const allowUrls = new Set();
|
|
47
|
+
const re = /\ballow\b\s*:\s*(?<allowUrl>[^*\s]+)/ig;
|
|
48
|
+
const matches = body.matchAll(re);
|
|
49
|
+
let allowUrl;
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
for (const match of matches) {
|
|
52
|
+
allowUrl = match?.groups?.allowUrl;
|
|
53
|
+
if (allowUrl) {
|
|
54
|
+
allowUrls.add(allowUrl);
|
|
55
|
+
}
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
return
|
|
58
|
+
return allowUrls.size > 0 ? Array.from(allowUrls) : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get all the unique sitemap urls from robots.txt.
|
|
63
|
+
*
|
|
64
|
+
* @param {String} body - The robots.txt file
|
|
65
|
+
* @returns {Array} collection of unqiue sitemap urls
|
|
66
|
+
*/
|
|
67
|
+
function getSitemapUrls (body) {
|
|
68
|
+
const sitemapUrls = new Set();
|
|
69
|
+
const re = /\bsitemap\b\s*:\s*(?<sitemapUrl>[^\s]+)/ig;
|
|
70
|
+
const matches = body.matchAll(re);
|
|
71
|
+
let sitemapUrl;
|
|
72
|
+
|
|
73
|
+
for (const match of matches) {
|
|
74
|
+
sitemapUrl = match?.groups?.sitemapUrl;
|
|
75
|
+
if (sitemapUrl) {
|
|
76
|
+
sitemapUrls.add(sitemapUrl);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return sitemapUrls.size > 0 ? Array.from(sitemapUrls) : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse urls from robots.txt and generate input.
|
|
85
|
+
* Look for sitemaps first, then look for allow urls.
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} options - robots.txt options
|
|
88
|
+
* @param {String} body - robots.txt content
|
|
89
|
+
* @returns {Promise} resolves on success
|
|
90
|
+
*/
|
|
91
|
+
function processRobotsTxt (options, body) {
|
|
92
|
+
let allowUrls = false;
|
|
93
|
+
const urls = getSitemapUrls(body) || (allowUrls = getAllowUrls(body));
|
|
94
|
+
|
|
95
|
+
if (allowUrls) {
|
|
96
|
+
let result;
|
|
97
|
+
for (const url of urls) {
|
|
98
|
+
result = base.input(options, url);
|
|
99
|
+
// if base.input returns false, then no outputDir was defined,
|
|
100
|
+
// which is a fatal bad option problem and will happen immediately.
|
|
101
|
+
if (!result) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
common.prependMsgToErr(base.generatorError(), url, true)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return Promise.resolve();
|
|
108
|
+
} else {
|
|
109
|
+
if (urls && urls.length > 0) {
|
|
110
|
+
return smc.process(options, urls);
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`No urls found in ${options.source}`);
|
|
113
|
+
}
|
|
55
114
|
}
|
|
56
115
|
|
|
57
116
|
/**
|
|
@@ -60,54 +119,37 @@ function oneline (line, options) {
|
|
|
60
119
|
* @param {Object} options - Robots.txt options
|
|
61
120
|
* @param {String} options.source - The URL to a robots.txt
|
|
62
121
|
* @param {Function} options.timeout - Returns the robots.txt request timeout.
|
|
63
|
-
* @
|
|
122
|
+
* @returns {Promise} resolves to undefined.
|
|
64
123
|
*/
|
|
65
|
-
function getRobotsUrl (options
|
|
66
|
-
|
|
124
|
+
async function getRobotsUrl (options) {
|
|
125
|
+
const { default:got } = await import("got");
|
|
126
|
+
return got({
|
|
67
127
|
url: options.source,
|
|
68
|
-
timeout:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
128
|
+
timeout: {
|
|
129
|
+
request: options.timeout()
|
|
130
|
+
}
|
|
131
|
+
}).then(res => {
|
|
132
|
+
const error = common.checkResponse(res, "text/plain");
|
|
72
133
|
if (error) {
|
|
73
|
-
|
|
74
|
-
} else {
|
|
75
|
-
body.toString().split('\n').every(line => {
|
|
76
|
-
// Process the line input, but break if base.input returns false.
|
|
77
|
-
// For now, this can only happen if no outputDir is defined,
|
|
78
|
-
// which is a fatal bad option problem and will happen immediately.
|
|
79
|
-
if (!oneline(line, options)) {
|
|
80
|
-
error = common.prependMsgToErr(base.generatorError(), line, true);
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
return true;
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
callback(error);
|
|
134
|
+
throw new Error(error);
|
|
87
135
|
}
|
|
136
|
+
return processRobotsTxt(options, res.body.toString());
|
|
137
|
+
}).catch(err => {
|
|
138
|
+
throw new Error(common.prependMsgToErr(err, options.source, true));
|
|
88
139
|
});
|
|
89
140
|
}
|
|
90
141
|
|
|
91
142
|
/**
|
|
92
143
|
* Reads the robots.txt file and parses it.
|
|
144
|
+
*
|
|
145
|
+
* @param {Object} options - Robots.txt options
|
|
93
146
|
*/
|
|
94
|
-
function getRobotsFile (options
|
|
95
|
-
fs.readFile(options.source
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// which is a fatal bad option problem and will happen immediately.
|
|
101
|
-
if (!oneline(line, options)) {
|
|
102
|
-
err = common.prependMsgToErr(base.generatorError(), line, true);
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
return true;
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
callback(err);
|
|
110
|
-
});
|
|
147
|
+
function getRobotsFile (options) {
|
|
148
|
+
return fs.readFile(options.source)
|
|
149
|
+
.then(data => processRobotsTxt(options, data.toString()))
|
|
150
|
+
.catch(err => {
|
|
151
|
+
throw new Error(common.prependMsgToErr(err, options.source, true));
|
|
152
|
+
});
|
|
111
153
|
}
|
|
112
154
|
|
|
113
155
|
/**
|
|
@@ -122,10 +164,8 @@ function getRobotsFile (options, callback) {
|
|
|
122
164
|
* @returns {Promise} Resolves to undefined on completion.
|
|
123
165
|
*/
|
|
124
166
|
function generateInput (options) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
options
|
|
128
|
-
)
|
|
167
|
+
const retrieve = common.isUrl(options.source) ? getRobotsUrl : getRobotsFile;
|
|
168
|
+
return retrieve(options)
|
|
129
169
|
.catch(err => {
|
|
130
170
|
options._abort(err);
|
|
131
171
|
})
|
|
@@ -7,16 +7,8 @@
|
|
|
7
7
|
* Copyright (c) 2013 - 2022 Alex Grant, LocalNerve, contributors
|
|
8
8
|
* Licensed under the MIT license.
|
|
9
9
|
*/
|
|
10
|
-
/* global Promise */
|
|
11
|
-
|
|
12
|
-
const fs = require("fs");
|
|
13
|
-
const pathLib = require("path");
|
|
14
|
-
const _ = require("lodash");
|
|
15
|
-
const mkdirp = require("mkdirp");
|
|
16
|
-
const xml2js = require("xml2js");
|
|
17
10
|
const common = require("../common");
|
|
18
|
-
const
|
|
19
|
-
const smLib = require("../common/sitemap");
|
|
11
|
+
const { sitemap: sm } = require("../common/sitemap");
|
|
20
12
|
const base = require("./_base");
|
|
21
13
|
|
|
22
14
|
// The default options
|
|
@@ -44,116 +36,6 @@ const sitemapIndex = {
|
|
|
44
36
|
}
|
|
45
37
|
};
|
|
46
38
|
|
|
47
|
-
/**
|
|
48
|
-
* Make sure path exists so a sitemap write can succeed.
|
|
49
|
-
*
|
|
50
|
-
* @param {String} outputPath - File path and name.
|
|
51
|
-
* @param {Function} callback - Completion callback.
|
|
52
|
-
*/
|
|
53
|
-
function prepareWrite (outputPath, callback) {
|
|
54
|
-
const path = pathLib.parse(outputPath);
|
|
55
|
-
const dir = pathLib.join(path.root, path.dir);
|
|
56
|
-
mkdirp(dir).then(() => callback()).catch(callback);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Parse a sitemap, generate input, and optionally update the local file
|
|
61
|
-
* for sitemapPolicy purposes.
|
|
62
|
-
* This writes the sitemap files out EVEN IF sitemapPolicy is false.
|
|
63
|
-
* The thinking is that it's better/cheaper to do this b/c if sitemapPolicy is
|
|
64
|
-
* enabled later, then there is something to compare against, for more savings.
|
|
65
|
-
*
|
|
66
|
-
* @param {Object} options - Sitemap options.
|
|
67
|
-
* @param {Object} options.__sitemapIndex - Sitemap Index options.
|
|
68
|
-
* @param {String} document - xml document string.
|
|
69
|
-
* @param {Function} callback - A completion callback.
|
|
70
|
-
*/
|
|
71
|
-
function processSitemap (options, document, callback) {
|
|
72
|
-
smLib.parse(options, document, err => {
|
|
73
|
-
const sitemapIndexOpts = options.__sitemapIndex;
|
|
74
|
-
const outputPath = base.outputFile(sitemapIndexOpts, options.source);
|
|
75
|
-
|
|
76
|
-
if (!err && sitemapIndexOpts.sitemapOutputDir) {
|
|
77
|
-
prepareWrite(outputPath, err => {
|
|
78
|
-
if (!err) {
|
|
79
|
-
fs.writeFile(outputPath, document, callback);
|
|
80
|
-
} else {
|
|
81
|
-
callback(common.prependMsgToErr(err, outputPath, true));
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
} else {
|
|
85
|
-
callback(err);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Parse a sitemap index document
|
|
92
|
-
* For each qualifying sitemap element, download, parse the sitemap.
|
|
93
|
-
* Stops processing if an error occurs.
|
|
94
|
-
*
|
|
95
|
-
* @param {Object} options - Sitemap options.
|
|
96
|
-
* @param {Boolean} options.sitemapPolicy - True if sitemap policy should be used.
|
|
97
|
-
* @param {String} document - xml document string.
|
|
98
|
-
* @param {Function} callback - A completion callback.
|
|
99
|
-
*/
|
|
100
|
-
function parse (options, document, callback) {
|
|
101
|
-
xml2js.parseString(document, {
|
|
102
|
-
trim: true,
|
|
103
|
-
normalizeTags: true,
|
|
104
|
-
normalize: true
|
|
105
|
-
}, (err, result) => {
|
|
106
|
-
const sitemapUrls = [];
|
|
107
|
-
const sitemapIndexOptions = Object.assign({}, options, {
|
|
108
|
-
outputPath: undefined
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
if (!err) {
|
|
112
|
-
// Check if we should process each sitemap in the index.
|
|
113
|
-
_.forEach(
|
|
114
|
-
// if the sitemap index is malformed, just blow up
|
|
115
|
-
result.sitemapindex.sitemap,
|
|
116
|
-
sitemapNode => {
|
|
117
|
-
// optionally ignore current sitemaps by sitemap policy
|
|
118
|
-
const shouldProcess = !options.sitemapPolicy ||
|
|
119
|
-
!smLib.stillCurrent(sitemapNode, sitemapIndexOptions);
|
|
120
|
-
|
|
121
|
-
if (shouldProcess) {
|
|
122
|
-
// if sitemap index is malformed, just blow up
|
|
123
|
-
sitemapUrls.push(sitemapNode.loc[0]);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
// Edge case message for clarity
|
|
129
|
-
if (sitemapUrls.length === 0) {
|
|
130
|
-
console.log("[*] No sitemaps qualified for processing");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Get all the sitemaps, parse them, and generate input for each
|
|
134
|
-
Promise.all(sitemapUrls.map(sitemapUrl => {
|
|
135
|
-
const sitemapOptions = Object.assign({}, options, {
|
|
136
|
-
source: sitemapUrl,
|
|
137
|
-
input: "sitemap",
|
|
138
|
-
sitemapOutputDir: false,
|
|
139
|
-
__sitemapIndex: sitemapIndexOptions
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
console.log(`[+] Loading the sitemap: '${sitemapUrl}'`);
|
|
143
|
-
|
|
144
|
-
return nodeCall(smLib.getUrl, sitemapOptions, processSitemap);
|
|
145
|
-
}))
|
|
146
|
-
.then(() => {
|
|
147
|
-
// Ignore the array of results to this function
|
|
148
|
-
callback();
|
|
149
|
-
})
|
|
150
|
-
.catch(callback);
|
|
151
|
-
} else {
|
|
152
|
-
callback(err);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
39
|
/**
|
|
158
40
|
* generateInput
|
|
159
41
|
*
|
|
@@ -165,11 +47,8 @@ function parse (options, document, callback) {
|
|
|
165
47
|
* @param {String} options.source - Sitemap URL or file path.
|
|
166
48
|
*/
|
|
167
49
|
function generateInput (options) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
options,
|
|
171
|
-
parse
|
|
172
|
-
)
|
|
50
|
+
const retrieve = common.isUrl(options.source) ? sm.getUrl : sm.getFile;
|
|
51
|
+
return retrieve(options, sm.parse)
|
|
173
52
|
.catch(err => {
|
|
174
53
|
options._abort(err);
|
|
175
54
|
})
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const common = require("../common");
|
|
12
|
-
const
|
|
13
|
-
const smLib = require("../common/sitemap");
|
|
12
|
+
const { sitemap: sm } = require("../common/sitemap");
|
|
14
13
|
const base = require("./_base");
|
|
15
14
|
|
|
16
15
|
// The default options
|
|
@@ -48,11 +47,8 @@ const sitemap = {
|
|
|
48
47
|
* @returns {Promise} Resolves to undefined on completion.
|
|
49
48
|
*/
|
|
50
49
|
function generateInput (options) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
options,
|
|
54
|
-
smLib.parse
|
|
55
|
-
)
|
|
50
|
+
const retrieve = common.isUrl(options.source) ? sm.getUrl : sm.getFile;
|
|
51
|
+
return retrieve(options, sm.parse)
|
|
56
52
|
.catch(err => {
|
|
57
53
|
options._abort(err);
|
|
58
54
|
})
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
* Licensed under the MIT license.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const fs = require("fs");
|
|
11
|
+
const fs = require("fs").promises;
|
|
12
12
|
const base = require("./_base");
|
|
13
13
|
const common = require("../common");
|
|
14
|
-
const nodeCall = require("../common/node");
|
|
15
14
|
|
|
16
15
|
// The default options
|
|
17
16
|
const defaults = Object.freeze({
|
|
@@ -44,10 +43,7 @@ const textfile = {
|
|
|
44
43
|
* @returns {Promise} resolves when complete.
|
|
45
44
|
*/
|
|
46
45
|
function generateInput (options) {
|
|
47
|
-
return
|
|
48
|
-
fs.readFile,
|
|
49
|
-
options.source
|
|
50
|
-
)
|
|
46
|
+
return fs.readFile(options.source)
|
|
51
47
|
.catch(err => {
|
|
52
48
|
options._abort(err);
|
|
53
49
|
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --unhandled-rejections=strict
|
|
2
|
+
/**
|
|
3
|
+
* puppeteer process entry point
|
|
4
|
+
*
|
|
5
|
+
* Command line arguments:
|
|
6
|
+
* 0 - /path/to/node
|
|
7
|
+
* 1 - *this script*
|
|
8
|
+
* 2 - outputFile
|
|
9
|
+
* 3 - url
|
|
10
|
+
* 4 - selector
|
|
11
|
+
* [5] - timeout
|
|
12
|
+
* [6] - filter
|
|
13
|
+
* [7] - debug
|
|
14
|
+
* [8] - slowMo
|
|
15
|
+
*/
|
|
16
|
+
const puppeteer = require('puppeteer');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { promises: fs } = require('fs');
|
|
19
|
+
|
|
20
|
+
if (process.argv.length < 5) {
|
|
21
|
+
throw Error(`puppeteer script '${
|
|
22
|
+
process.argv[1]
|
|
23
|
+
}' expected these arguments:\n\
|
|
24
|
+
OUTPUTFILE URL SELECTOR [TIMEOUT FILTER DEBUG SLOWMO]`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const options = {
|
|
28
|
+
outputFile: process.argv[2],
|
|
29
|
+
url: process.argv[3],
|
|
30
|
+
selector: process.argv[4],
|
|
31
|
+
timeout: process.argv[5] ? parseInt(process.argv[5], 10) : 10000,
|
|
32
|
+
filter: process.argv[6] && process.argv[6] !== 'false' ? process.argv[6] : null,
|
|
33
|
+
debug: process.argv[7] && process.argv[7].toLowerCase() === 'true' ? true : false,
|
|
34
|
+
slowMo: process.argv[8] ? parseInt(process.argv[8], 10) : 500
|
|
35
|
+
};
|
|
36
|
+
const launchOptions = options.debug ? {
|
|
37
|
+
headless: false,
|
|
38
|
+
slowMo: options.slowMo,
|
|
39
|
+
devtools: true
|
|
40
|
+
} : {};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run the snapshot script.
|
|
44
|
+
*
|
|
45
|
+
* @returns {Promise} on completion
|
|
46
|
+
*/
|
|
47
|
+
async function run () {
|
|
48
|
+
let timeLeft, browser;
|
|
49
|
+
const start = Date.now();
|
|
50
|
+
|
|
51
|
+
timeLeft = options.timeout - 200; // leave some room for reporting
|
|
52
|
+
if (timeLeft < 0) timeLeft = 0;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const filter =
|
|
56
|
+
options.filter ? require(options.filter) : passthru => passthru;
|
|
57
|
+
|
|
58
|
+
browser = await puppeteer.launch(launchOptions);
|
|
59
|
+
const page = await browser.newPage();
|
|
60
|
+
await page.goto(options.url, {
|
|
61
|
+
timeout: timeLeft
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
timeLeft -= (Date.now() - start);
|
|
65
|
+
if (timeLeft < 0) timeLeft = 0;
|
|
66
|
+
|
|
67
|
+
await fs.mkdir(path.dirname(options.outputFile), {
|
|
68
|
+
recursive: true
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await page.waitForSelector(options.selector, {
|
|
72
|
+
Visible: true,
|
|
73
|
+
timeout: timeLeft
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const content = await page.content();
|
|
77
|
+
await fs.writeFile(options.outputFile, filter(content));
|
|
78
|
+
const elapsed = Date.now() - start;
|
|
79
|
+
|
|
80
|
+
await browser.close();
|
|
81
|
+
|
|
82
|
+
return `snapshot for ${options.url} finished in ${elapsed} ms\n\
|
|
83
|
+
written to ${options.outputFile}`;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
const err = new Error(`failed to snapshot ${options.url}`, {
|
|
86
|
+
cause: e
|
|
87
|
+
});
|
|
88
|
+
if (browser) {
|
|
89
|
+
browser.close().finally(() => {
|
|
90
|
+
throw err;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
run().then(console.log).catch(e => {
|
|
97
|
+
throw e;
|
|
98
|
+
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "html-snapshots",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "Alex Grant",
|
|
6
6
|
"email": "alex@localnerve.com",
|
|
7
|
-
"url": "
|
|
7
|
+
"url": "https://www.localnerve.com"
|
|
8
8
|
},
|
|
9
9
|
"homepage": "https://github.com/localnerve/html-snapshots",
|
|
10
10
|
"description": "A selector-based html snapshot tool using PhantomJS that sources sitemap.xml, robots.txt, or arbitrary input",
|
|
@@ -22,8 +22,12 @@
|
|
|
22
22
|
"test": "mocha test/mocha/**/*.js --exit --recursive --reporter spec",
|
|
23
23
|
"test:async": "mocha test/mocha/async/test.js --exit --reporter spec",
|
|
24
24
|
"test:common": "mocha test/mocha/common/*.js --exit --reporter spec",
|
|
25
|
+
"test:browsers": "mocha test/mocha/browsers/test.js --exit --reporter spec",
|
|
26
|
+
"test:browsers:cover": "c8 -- npm run test:browsers",
|
|
25
27
|
"test:input-generators": "mocha test/mocha/input-generators/*.js --exit --reporter spec",
|
|
28
|
+
"test:input-generators:debug": "mocha --inspect-brk test/mocha/input-generators/*.js --exit --reporter spec",
|
|
26
29
|
"test:html-snapshots": "mocha test/mocha/html-snapshots/*.js --exit --reporter spec",
|
|
30
|
+
"test:html-snapshots:debug": "mocha --inspect-brk test/mocha/html-snapshots/*.js --exit --reporter spec",
|
|
27
31
|
"test:cover": "c8 -- npm test",
|
|
28
32
|
"validate": "npm ls"
|
|
29
33
|
},
|
|
@@ -49,15 +53,16 @@
|
|
|
49
53
|
"async": "3.2.4",
|
|
50
54
|
"async-lock": "1.3.2",
|
|
51
55
|
"combine-errors": "3.0.3",
|
|
56
|
+
"got": "12.5.0",
|
|
52
57
|
"lodash": "4.17.21",
|
|
53
58
|
"mkdirp": "1.0.4",
|
|
54
59
|
"phantomjs-prebuilt": "2.1.16",
|
|
55
|
-
"
|
|
60
|
+
"puppeteer": "^18.0.5",
|
|
56
61
|
"rimraf": "3.0.2",
|
|
57
62
|
"xml2js": "0.4.23"
|
|
58
63
|
},
|
|
59
64
|
"devDependencies": {
|
|
60
|
-
"eslint": "8.
|
|
65
|
+
"eslint": "8.23.1",
|
|
61
66
|
"express": "4.18.1",
|
|
62
67
|
"mocha": "10.0.0",
|
|
63
68
|
"c8": "7.12.0",
|
package/test/helpers/options.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* If a global phantomjs is defined, decorates html-snapshots options to specify that global
|
|
10
10
|
* In some test environments (travis), local phantomjs will not install if a global is found.
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
const assert = require("assert");
|
|
13
13
|
|
|
14
14
|
// @0.12.0, on Travis-ci phantomjs2 WILL install local WITH the older global.
|
|
15
15
|
// So reverting back to decorateLocal only.
|
|
@@ -19,7 +19,9 @@ var assert = require("assert");
|
|
|
19
19
|
function fillAbort (options) {
|
|
20
20
|
if (!options._abort) {
|
|
21
21
|
options._abort = function (err) {
|
|
22
|
-
|
|
22
|
+
const msg = `[options._abort] called: ${err.toString()}`;
|
|
23
|
+
console.log(`@@@ ${msg}`);
|
|
24
|
+
assert.fail(msg);
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests that target the puppeteer script specifically.
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2013 - 2022, Alex Grant, LocalNerve, contributors
|
|
5
|
+
*/
|
|
6
|
+
/* global before, it */
|
|
7
|
+
const assert = require("assert");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const rimraf = require("rimraf").sync;
|
|
10
|
+
const spawn = require("child_process").spawn;
|
|
11
|
+
const server = require("../../server");
|
|
12
|
+
|
|
13
|
+
const outputDir = path.join(__dirname, "tmp");
|
|
14
|
+
const port = 9340;
|
|
15
|
+
|
|
16
|
+
function puppeteerTests () {
|
|
17
|
+
const snapshotScript = path.resolve(__dirname, '../../../lib/puppeteer/index.js');
|
|
18
|
+
const outputFile = path.join(outputDir, "snapshot.html");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Launch the puppeteer script as a process.
|
|
22
|
+
*
|
|
23
|
+
* @param {Array} args - array of puppeteer args
|
|
24
|
+
*/
|
|
25
|
+
function spawnPuppeteer (args, done) {
|
|
26
|
+
const cp = spawn(
|
|
27
|
+
snapshotScript,
|
|
28
|
+
args
|
|
29
|
+
, { cwd: process.cwd(), stdio: "inherit", detached: true }
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
cp.on("exit", code => {
|
|
33
|
+
if (code !== 0) {
|
|
34
|
+
return done(new Error(`error exit code: ${code}`));
|
|
35
|
+
}
|
|
36
|
+
done();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return function () {
|
|
41
|
+
before(function() {
|
|
42
|
+
server.start(path.join(__dirname, "./server"), port);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should succeed with minimal args", function (done) {
|
|
46
|
+
this.timeout(5000);
|
|
47
|
+
|
|
48
|
+
rimraf(outputDir);
|
|
49
|
+
|
|
50
|
+
spawnPuppeteer([
|
|
51
|
+
outputFile,
|
|
52
|
+
`http://localhost:${port}/`,
|
|
53
|
+
"body"
|
|
54
|
+
], done);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should fail if insufficient input args", function (done) {
|
|
58
|
+
rimraf(outputDir);
|
|
59
|
+
|
|
60
|
+
spawnPuppeteer([
|
|
61
|
+
outputFile,
|
|
62
|
+
"http://someurl.domain/path/resource"
|
|
63
|
+
], e => {
|
|
64
|
+
const msg = "expected process error";
|
|
65
|
+
assert.ok(e, msg);
|
|
66
|
+
done(e ? undefined : new Error(msg));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should fail with bad url", function (done) {
|
|
71
|
+
rimraf(outputDir);
|
|
72
|
+
|
|
73
|
+
spawnPuppeteer([
|
|
74
|
+
outputFile,
|
|
75
|
+
"http://nonexistent.domain/path/resource",
|
|
76
|
+
"body",
|
|
77
|
+
1000
|
|
78
|
+
], e => {
|
|
79
|
+
const msg = "expected process error";
|
|
80
|
+
assert.ok(e, msg);
|
|
81
|
+
done(e ? undefined : new Error(msg));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should launch debugging", function (done) {
|
|
86
|
+
const timeout = 5000;
|
|
87
|
+
this.timeout(timeout);
|
|
88
|
+
|
|
89
|
+
rimraf(outputDir);
|
|
90
|
+
|
|
91
|
+
spawnPuppeteer([
|
|
92
|
+
outputFile,
|
|
93
|
+
`http://localhost:${port}/`,
|
|
94
|
+
"body",
|
|
95
|
+
timeout - 1000,
|
|
96
|
+
"false",
|
|
97
|
+
"true",
|
|
98
|
+
10
|
|
99
|
+
], done);
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
testSuite: puppeteerTests
|
|
106
|
+
};
|