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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@single-file/single-file-cli",
3
- "version": "2.0.18",
3
+ "version": "2.0.20",
4
4
  "description": "SingleFile CLI",
5
5
  "nodeModulesDir": true,
6
6
  "exports": {
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 NETWORK_IDLE_STATE = "networkIdle";
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", async ({ params }) => {
66
- await Fetch.continueWithAuth({
67
- requestId: params.requestId,
68
- authChallengeResponse: {
69
- response: "ProvideCredentials",
70
- username: options.httpProxyUsername,
71
- password: options.httpProxyPassword
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 Runtime.enable();
122
- const topFrameId = targetInfo.id;
123
- let contextId;
124
- const CONTEXT_CREATED_EVENT = "executionContextCreated";
125
- Runtime.addEventListener(CONTEXT_CREATED_EVENT, executionContextCreated);
126
- await Page.enable();
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.18";
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 (netWorkIdle, netWorkAlmostIdle, load, domContentLoaded)", type: "string", defaultValue: "networkIdle" },
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[]" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-cli",
3
- "version": "2.0.18",
3
+ "version": "2.0.20",
4
4
  "description": "SingleFile CLI",
5
5
  "author": "Gildas Lormeau",
6
6
  "engines": {