single-file-cli 2.0.18 → 2.0.20
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 +0 -13
- package/deno.json +1 -1
- package/lib/cdp-client.js +178 -100
- package/lib/version.js +1 -1
- package/options.js +1 -1
- package/package.json +1 -1
package/README.MD
CHANGED
|
@@ -80,9 +80,6 @@ Make sure Chrome or a Chromium-based browser is installed in the default folder.
|
|
|
80
80
|
|
|
81
81
|
```sh
|
|
82
82
|
unzip master.zip .
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
```sh
|
|
86
83
|
cd single-file-cli-master
|
|
87
84
|
```
|
|
88
85
|
|
|
@@ -90,9 +87,6 @@ Make sure Chrome or a Chromium-based browser is installed in the default folder.
|
|
|
90
87
|
|
|
91
88
|
```sh
|
|
92
89
|
git clone --depth 1 --recursive https://github.com/gildas-lormeau/single-file-cli.git
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
```sh
|
|
96
90
|
cd single-file-cli
|
|
97
91
|
```
|
|
98
92
|
|
|
@@ -130,13 +124,6 @@ Make sure Chrome or a Chromium-based browser is installed in the default folder.
|
|
|
130
124
|
single-file https://www.wikipedia.org wikipedia.html
|
|
131
125
|
```
|
|
132
126
|
|
|
133
|
-
- Save https://www.wikipedia.org into `wikipedia.html` in the current folder
|
|
134
|
-
with Firefox instead of Chrome
|
|
135
|
-
|
|
136
|
-
```sh
|
|
137
|
-
single-file https://www.wikipedia.org wikipedia.html --back-end=webdriver-gecko
|
|
138
|
-
```
|
|
139
|
-
|
|
140
127
|
- Save a list of URLs stored into `list-urls.txt` in the current folder
|
|
141
128
|
|
|
142
129
|
```sh
|
package/deno.json
CHANGED
package/lib/cdp-client.js
CHANGED
|
@@ -28,8 +28,8 @@ import { getScriptSource, getHookScriptSource } from "./single-file-script.js";
|
|
|
28
28
|
import { CDP } from "simple-cdp";
|
|
29
29
|
|
|
30
30
|
const LOAD_TIMEOUT_ERROR = "ERR_LOAD_TIMEOUT";
|
|
31
|
-
const
|
|
32
|
-
const NETWORK_STATES = ["networkAlmostIdle", "load", "DOMContentLoaded"];
|
|
31
|
+
const INTERACTIVE_TIME_STATE = "InteractiveTime";
|
|
32
|
+
const NETWORK_STATES = ["InteractiveTime", "networkIdle", "networkAlmostIdle", "load", "DOMContentLoaded"];
|
|
33
33
|
const MINIMIZED_WINDOW_STATE = "minimized";
|
|
34
34
|
const SINGLE_FILE_WORLD_NAME = "singlefile";
|
|
35
35
|
const EMPTY_PAGE_URL = "about:blank";
|
|
@@ -62,16 +62,14 @@ async function getPageData(options) {
|
|
|
62
62
|
}
|
|
63
63
|
if (options.httpProxyServer && options.httpProxyUsername) {
|
|
64
64
|
await Fetch.enable({ handleAuthRequests: true });
|
|
65
|
-
Fetch.addEventListener("authRequired",
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
74
|
-
});
|
|
65
|
+
Fetch.addEventListener("authRequired", ({ params }) => Fetch.continueWithAuth({
|
|
66
|
+
requestId: params.requestId,
|
|
67
|
+
authChallengeResponse: {
|
|
68
|
+
response: "ProvideCredentials",
|
|
69
|
+
username: options.httpProxyUsername,
|
|
70
|
+
password: options.httpProxyPassword
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
75
73
|
Fetch.addEventListener("requestPaused", ({ params }) => Fetch.continueRequest({ requestId: params.requestId }));
|
|
76
74
|
}
|
|
77
75
|
if (options.httpHeaders && options.httpHeaders.length) {
|
|
@@ -88,27 +86,6 @@ async function getPageData(options) {
|
|
|
88
86
|
if (options.browserCookies && options.browserCookies.length) {
|
|
89
87
|
await Network.setCookies({ cookies: options.browserCookies });
|
|
90
88
|
}
|
|
91
|
-
let debuggerReady;
|
|
92
|
-
if (options.browserDebug) {
|
|
93
|
-
await Debugger.enable();
|
|
94
|
-
debuggerReady = new Promise(resolve => {
|
|
95
|
-
const SCRIPT_PARSED_EVENT = "scriptParsed";
|
|
96
|
-
const RESUMED_EVENT = "resumed";
|
|
97
|
-
Debugger.addEventListener(SCRIPT_PARSED_EVENT, onScriptParsed);
|
|
98
|
-
|
|
99
|
-
async function onScriptParsed() {
|
|
100
|
-
Debugger.removeEventListener(SCRIPT_PARSED_EVENT, onScriptParsed);
|
|
101
|
-
Debugger.addEventListener(RESUMED_EVENT, onResumed);
|
|
102
|
-
await Debugger.pause();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function onResumed() {
|
|
106
|
-
Debugger.removeEventListener(RESUMED_EVENT, onResumed);
|
|
107
|
-
await Debugger.disable();
|
|
108
|
-
resolve();
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
89
|
await Page.addScriptToEvaluateOnNewDocument({
|
|
113
90
|
source: getHookScriptSource(),
|
|
114
91
|
runImmediately: true
|
|
@@ -118,65 +95,12 @@ async function getPageData(options) {
|
|
|
118
95
|
runImmediately: true,
|
|
119
96
|
worldName: SINGLE_FILE_WORLD_NAME
|
|
120
97
|
});
|
|
121
|
-
await
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
await Page.setLifecycleEventsEnabled({ enabled: true });
|
|
128
|
-
const pageNavigated = Page.navigate({ url: options.url });
|
|
129
|
-
const pageReady = new Promise(resolve => {
|
|
130
|
-
const LIFE_CYCLE_EVENT = "lifecycleEvent";
|
|
131
|
-
const FRAME_NAVIGATED_EVENT = "frameNavigated";
|
|
132
|
-
let contentLoaded;
|
|
133
|
-
Page.addEventListener(LIFE_CYCLE_EVENT, onLifecycleEvent);
|
|
134
|
-
Page.addEventListener(FRAME_NAVIGATED_EVENT, onFrameNavigated);
|
|
135
|
-
|
|
136
|
-
async function onLifecycleEvent({ params }) {
|
|
137
|
-
const { name, frameId } = params;
|
|
138
|
-
if (frameId === topFrameId) {
|
|
139
|
-
if (!contentLoaded && name === "init" || name === "firstPaint") {
|
|
140
|
-
contentLoaded = true;
|
|
141
|
-
}
|
|
142
|
-
if (contentLoaded && name === (options.browserWaitUntil || NETWORK_IDLE_STATE)) {
|
|
143
|
-
if (options.browserWaitDelay) {
|
|
144
|
-
setTimeout(() => resolve, options.browserWaitDelay);
|
|
145
|
-
} else {
|
|
146
|
-
Page.removeEventListener(LIFE_CYCLE_EVENT, onLifecycleEvent);
|
|
147
|
-
Page.removeEventListener(FRAME_NAVIGATED_EVENT, onFrameNavigated);
|
|
148
|
-
await Page.setLifecycleEventsEnabled({ enabled: false });
|
|
149
|
-
await Page.disable();
|
|
150
|
-
resolve();
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function onFrameNavigated({ params }) {
|
|
157
|
-
const { frame } = params;
|
|
158
|
-
if (!frame.parentId) {
|
|
159
|
-
contextId = undefined;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
let timeoutReady, timeoutId, resolveTimeoutReady;
|
|
164
|
-
if (options.browserLoadMaxTime) {
|
|
165
|
-
timeoutReady = new Promise((resolve, reject) => {
|
|
166
|
-
resolveTimeoutReady = resolve;
|
|
167
|
-
timeoutId = setTimeout(() => {
|
|
168
|
-
const error = new Error("Load timeout");
|
|
169
|
-
error.code = LOAD_TIMEOUT_ERROR;
|
|
170
|
-
reject(error);
|
|
171
|
-
}, options.browserLoadMaxTime);
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
await Promise.race([Promise.all([pageNavigated, pageReady, debuggerReady]), timeoutReady]);
|
|
175
|
-
Runtime.removeEventListener(CONTEXT_CREATED_EVENT, executionContextCreated);
|
|
176
|
-
await Runtime.disable();
|
|
177
|
-
if (timeoutId) {
|
|
178
|
-
clearTimeout(timeoutId);
|
|
179
|
-
resolveTimeoutReady();
|
|
98
|
+
const [contextId] = await Promise.all([
|
|
99
|
+
loadPage({ Page, Runtime }, options),
|
|
100
|
+
options.browserDebug ? waitForDebuggerReady({ Debugger }) : Promise.resolve()
|
|
101
|
+
]);
|
|
102
|
+
if (options.browserWaitDelay) {
|
|
103
|
+
setTimeout(() => resolve, options.browserWaitDelay);
|
|
180
104
|
}
|
|
181
105
|
const { result } = await Runtime.evaluate({
|
|
182
106
|
expression: `singlefile.getPageData(${JSON.stringify(options)})`,
|
|
@@ -184,18 +108,14 @@ async function getPageData(options) {
|
|
|
184
108
|
returnByValue: true,
|
|
185
109
|
contextId
|
|
186
110
|
});
|
|
187
|
-
const { value } = result;
|
|
111
|
+
const { value, subtype, description } = result;
|
|
112
|
+
if (subtype === "error") {
|
|
113
|
+
throw new Error(description);
|
|
114
|
+
}
|
|
188
115
|
if (options.compressContent) {
|
|
189
116
|
value.content = new Uint8Array(value.content);
|
|
190
117
|
}
|
|
191
118
|
return value;
|
|
192
|
-
|
|
193
|
-
async function executionContextCreated({ params }) {
|
|
194
|
-
const { context } = params;
|
|
195
|
-
if (contextId === undefined && context.name === SINGLE_FILE_WORLD_NAME && context.auxData && context.auxData.frameId === topFrameId) {
|
|
196
|
-
contextId = context.id;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
119
|
} catch (error) {
|
|
200
120
|
if (error.code === LOAD_TIMEOUT_ERROR && options.browserWaitUntilFallback && options.browserWaitUntil) {
|
|
201
121
|
const browserWaitUntil = NETWORK_STATES[(NETWORK_STATES.indexOf(options.browserWaitUntil) + 1)];
|
|
@@ -212,6 +132,164 @@ async function getPageData(options) {
|
|
|
212
132
|
}
|
|
213
133
|
}
|
|
214
134
|
|
|
135
|
+
async function loadPage({ Page, Runtime }, options) {
|
|
136
|
+
await Runtime.enable();
|
|
137
|
+
await Page.enable();
|
|
138
|
+
try {
|
|
139
|
+
const loadTimeoutAbortController = new AbortController();
|
|
140
|
+
const loadTimeoutAbortSignal = loadTimeoutAbortController.signal;
|
|
141
|
+
const [contextId] = await Promise.race([
|
|
142
|
+
Promise.all([getTopFrameContextId({ Page, Runtime }, options), Page.navigate({ url: options.url })]),
|
|
143
|
+
waitForLoadTimeout(loadTimeoutAbortSignal, options.browserLoadMaxTime)
|
|
144
|
+
]);
|
|
145
|
+
if (!loadTimeoutAbortSignal.aborted) {
|
|
146
|
+
loadTimeoutAbortController.abort();
|
|
147
|
+
}
|
|
148
|
+
return contextId;
|
|
149
|
+
} finally {
|
|
150
|
+
await Runtime.disable();
|
|
151
|
+
await Page.disable();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getTopFrameContextId({ Page, Runtime }, options) {
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const FRAME_NAVIGATED_EVENT = "frameNavigated";
|
|
158
|
+
const CONTEXT_CREATED_EVENT = "executionContextCreated";
|
|
159
|
+
const CONTEXT_DESTROYED_EVENT = "executionContextDestroyed";
|
|
160
|
+
const contextIds = [];
|
|
161
|
+
let frameId, navigationAbortController;
|
|
162
|
+
Page.addEventListener(FRAME_NAVIGATED_EVENT, onFrameNavigated);
|
|
163
|
+
Runtime.addEventListener(CONTEXT_CREATED_EVENT, onExecutionContextCreated);
|
|
164
|
+
Runtime.addEventListener(CONTEXT_DESTROYED_EVENT, onExecutionContextDestroyed);
|
|
165
|
+
|
|
166
|
+
async function onFrameNavigated({ params }) {
|
|
167
|
+
const { frame } = params;
|
|
168
|
+
if (!frame.parentId) {
|
|
169
|
+
frameId = frame.id;
|
|
170
|
+
if (navigationAbortController) {
|
|
171
|
+
navigationAbortController.abort();
|
|
172
|
+
}
|
|
173
|
+
navigationAbortController = new AbortController();
|
|
174
|
+
await onTopFrameNavigated(navigationAbortController.signal);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function onTopFrameNavigated(navigationSignal) {
|
|
179
|
+
await waitForPageReady({ Page }, frameId, options, navigationSignal);
|
|
180
|
+
if (!navigationSignal.aborted) {
|
|
181
|
+
Page.removeEventListener(FRAME_NAVIGATED_EVENT, onFrameNavigated);
|
|
182
|
+
Runtime.removeEventListener(CONTEXT_CREATED_EVENT, onExecutionContextCreated);
|
|
183
|
+
Runtime.removeEventListener(CONTEXT_DESTROYED_EVENT, onExecutionContextDestroyed);
|
|
184
|
+
let contextId;
|
|
185
|
+
if (contextIds.length) {
|
|
186
|
+
let contextIdIndex = 0;
|
|
187
|
+
do {
|
|
188
|
+
if (await testExecutionContext(contextIds[contextIdIndex])) {
|
|
189
|
+
contextId = contextIds[contextIdIndex];
|
|
190
|
+
}
|
|
191
|
+
contextIdIndex++;
|
|
192
|
+
} while (contextId === undefined && contextIdIndex < contextIds.length);
|
|
193
|
+
}
|
|
194
|
+
if (contextId === undefined) {
|
|
195
|
+
reject(new Error("Execution context not found"));
|
|
196
|
+
} else {
|
|
197
|
+
resolve(contextId);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function onExecutionContextCreated({ params }) {
|
|
203
|
+
const { context } = params;
|
|
204
|
+
const { name, origin, auxData } = context;
|
|
205
|
+
if (frameId !== undefined && origin === "://" && name === SINGLE_FILE_WORLD_NAME && auxData && auxData.frameId === frameId) {
|
|
206
|
+
contextIds.push(context.id);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function onExecutionContextDestroyed({ params }) {
|
|
211
|
+
const { executionContextId } = params;
|
|
212
|
+
const index = contextIds.indexOf(executionContextId);
|
|
213
|
+
if (index > -1) {
|
|
214
|
+
contextIds.splice(index, 1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function testExecutionContext(contextId) {
|
|
219
|
+
try {
|
|
220
|
+
const { result } = await Runtime.evaluate({
|
|
221
|
+
expression: "singlefile !== undefined",
|
|
222
|
+
contextId
|
|
223
|
+
});
|
|
224
|
+
return result.value === true;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
// ignored
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function waitForLoadTimeout(abortSignal, maxDelay) {
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const ABORT_EVENT = "abort";
|
|
236
|
+
abortSignal.addEventListener(ABORT_EVENT, onAbort);
|
|
237
|
+
const timeoutId = setTimeout(() => {
|
|
238
|
+
const error = new Error("Load timeout");
|
|
239
|
+
error.code = LOAD_TIMEOUT_ERROR;
|
|
240
|
+
reject(error);
|
|
241
|
+
}, maxDelay);
|
|
242
|
+
|
|
243
|
+
function onAbort() {
|
|
244
|
+
abortSignal.removeEventListener(ABORT_EVENT, onAbort);
|
|
245
|
+
clearTimeout(timeoutId);
|
|
246
|
+
resolve();
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function waitForPageReady({ Page }, topFrameId, options, navigationSignal) {
|
|
252
|
+
await Page.setLifecycleEventsEnabled({ enabled: true });
|
|
253
|
+
try {
|
|
254
|
+
await new Promise(resolve => {
|
|
255
|
+
const LIFE_CYCLE_EVENT = "lifecycleEvent";
|
|
256
|
+
Page.addEventListener(LIFE_CYCLE_EVENT, onLifecycleEvent);
|
|
257
|
+
|
|
258
|
+
function onLifecycleEvent({ params }) {
|
|
259
|
+
const { name, frameId } = params;
|
|
260
|
+
if (frameId === topFrameId) {
|
|
261
|
+
if (name === options.browserWaitUntil || name === INTERACTIVE_TIME_STATE || navigationSignal.aborted) {
|
|
262
|
+
Page.removeEventListener(LIFE_CYCLE_EVENT, onLifecycleEvent);
|
|
263
|
+
resolve();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
} finally {
|
|
269
|
+
if (!navigationSignal.aborted) {
|
|
270
|
+
await Page.setLifecycleEventsEnabled({ enabled: false });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function waitForDebuggerReady({ Debugger }) {
|
|
276
|
+
await Debugger.enable();
|
|
277
|
+
try {
|
|
278
|
+
await Debugger.pause();
|
|
279
|
+
await new Promise(resolve => {
|
|
280
|
+
const RESUMED_EVENT = "resumed";
|
|
281
|
+
Debugger.addEventListener(RESUMED_EVENT, onResumed);
|
|
282
|
+
|
|
283
|
+
function onResumed() {
|
|
284
|
+
Debugger.removeEventListener(RESUMED_EVENT, onResumed);
|
|
285
|
+
resolve();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
} finally {
|
|
289
|
+
await Debugger.disable();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
215
293
|
function getBrowserOptions(options) {
|
|
216
294
|
const browserOptions = {};
|
|
217
295
|
browserOptions.args = options.browserArgs;
|
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "2.0.
|
|
1
|
+
export const version = "2.0.20";
|
package/options.js
CHANGED
|
@@ -49,7 +49,7 @@ const OPTIONS_INFO = {
|
|
|
49
49
|
"browser-height": { description: "Height of the browser viewport in pixels", type: "number", defaultValue: 720 },
|
|
50
50
|
"browser-load-max-time": { description: "Maximum delay of time to wait for page loading in ms", type: "number", defaultValue: 60000 },
|
|
51
51
|
"browser-wait-delay": { description: "Time to wait before capturing the page in ms", type: "number" },
|
|
52
|
-
"browser-wait-until": { description: "When to consider the page is loaded (
|
|
52
|
+
"browser-wait-until": { description: "When to consider the page is loaded (InteractiveTime, networkIdle, networkAlmostIdle, load, domContentLoaded)", type: "string", defaultValue: "networkIdle" },
|
|
53
53
|
"browser-wait-until-fallback": { description: "Retry with the next value of --browser-wait-until when a timeout error is thrown", type: "boolean", defaultValue: true },
|
|
54
54
|
"browser-debug": { description: "Enable debug mode", type: "boolean" },
|
|
55
55
|
"browser-script": { description: "Path of a script executed in the page (and all the frames) before it is loaded", type: "string[]" },
|