single-file-cli 2.0.17 → 2.0.19

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
@@ -130,13 +130,6 @@ Make sure Chrome or a Chromium-based browser is installed in the default folder.
130
130
  single-file https://www.wikipedia.org wikipedia.html
131
131
  ```
132
132
 
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
133
  - Save a list of URLs stored into `list-urls.txt` in the current folder
141
134
 
142
135
  ```sh
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@single-file/single-file-cli",
3
- "version": "2.0.17",
3
+ "version": "2.0.19",
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,66 +95,10 @@ 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 (name === "DOMContentLoaded") {
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();
180
- }
98
+ const [contextId] = await Promise.all([
99
+ navigate({ Page, Runtime }, options),
100
+ options.browserDebug ? waitForDebuggerReady({ Debugger }) : Promise.resolve()
101
+ ]);
181
102
  const { result } = await Runtime.evaluate({
182
103
  expression: `singlefile.getPageData(${JSON.stringify(options)})`,
183
104
  awaitPromise: true,
@@ -189,13 +110,6 @@ async function getPageData(options) {
189
110
  value.content = new Uint8Array(value.content);
190
111
  }
191
112
  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
113
  } catch (error) {
200
114
  if (error.code === LOAD_TIMEOUT_ERROR && options.browserWaitUntilFallback && options.browserWaitUntil) {
201
115
  const browserWaitUntil = NETWORK_STATES[(NETWORK_STATES.indexOf(options.browserWaitUntil) + 1)];
@@ -212,6 +126,111 @@ async function getPageData(options) {
212
126
  }
213
127
  }
214
128
 
129
+ async function navigate({ Page, Runtime }, options) {
130
+ await Runtime.enable();
131
+ await Page.enable();
132
+ try {
133
+ const loadTimeoutAbortController = new AbortController();
134
+ const loadTimeoutAbortSignal = loadTimeoutAbortController.signal;
135
+ const [contextId] = await Promise.race([
136
+ Promise.all([getTopFrameContextId({ Page, Runtime }, options), Page.navigate({ url: options.url })]),
137
+ options.browserLoadMaxTime ? waitForLoadTimeout(loadTimeoutAbortSignal, options.browserLoadMaxTime) : Promise.resolve()
138
+ ]);
139
+ if (!loadTimeoutAbortSignal.aborted) {
140
+ loadTimeoutAbortController.abort();
141
+ }
142
+ return contextId;
143
+ } finally {
144
+ await Runtime.disable();
145
+ await Page.disable();
146
+ }
147
+ }
148
+
149
+ async function getTopFrameContextId({ Page, Runtime }, options) {
150
+ let frameId, contextId;
151
+ await new Promise(resolve => {
152
+ const FRAME_NAVIGATED_EVENT = "frameNavigated";
153
+ const CONTEXT_CREATED_EVENT = "executionContextCreated";
154
+ Page.addEventListener(FRAME_NAVIGATED_EVENT, onFrameNavigated);
155
+ Runtime.addEventListener(CONTEXT_CREATED_EVENT, executionContextCreated);
156
+
157
+ async function onFrameNavigated({ params }) {
158
+ const { frame } = params;
159
+ if (!frame.parentId) {
160
+ frameId = frame.id;
161
+ await waitForPageReady({ Page }, frameId, options);
162
+ Page.removeEventListener(FRAME_NAVIGATED_EVENT, onFrameNavigated);
163
+ Runtime.removeEventListener(CONTEXT_CREATED_EVENT, executionContextCreated);
164
+ resolve();
165
+ }
166
+ }
167
+
168
+ function executionContextCreated({ params }) {
169
+ const { context } = params;
170
+ const { name, origin, auxData } = context;
171
+ if (frameId !== undefined && origin === "://" && name === SINGLE_FILE_WORLD_NAME && auxData && auxData.frameId === frameId) {
172
+ contextId = context.id;
173
+ }
174
+ }
175
+ });
176
+ return contextId;
177
+ }
178
+
179
+ function waitForLoadTimeout(abortSignal, maxDelay) {
180
+ return new Promise((resolve, reject) => {
181
+ const ABORT_EVENT = "abort";
182
+ abortSignal.addEventListener(ABORT_EVENT, onAbort);
183
+ const timeoutId = setTimeout(() => {
184
+ const error = new Error("Load timeout");
185
+ error.code = LOAD_TIMEOUT_ERROR;
186
+ reject(error);
187
+ }, maxDelay);
188
+
189
+ function onAbort() {
190
+ abortSignal.removeEventListener(ABORT_EVENT, onAbort);
191
+ clearTimeout(timeoutId);
192
+ resolve();
193
+ }
194
+ });
195
+ }
196
+
197
+ async function waitForPageReady({ Page }, topFrameId, options) {
198
+ await Page.setLifecycleEventsEnabled({ enabled: true });
199
+ await new Promise(resolve => {
200
+ const LIFE_CYCLE_EVENT = "lifecycleEvent";
201
+ Page.addEventListener(LIFE_CYCLE_EVENT, onLifecycleEvent);
202
+
203
+ function onLifecycleEvent({ params }) {
204
+ const { name, frameId } = params;
205
+ if (frameId === topFrameId) {
206
+ if ((name === options.browserWaitUntil || name === INTERACTIVE_TIME_STATE)) {
207
+ Page.removeEventListener(LIFE_CYCLE_EVENT, onLifecycleEvent);
208
+ resolve();
209
+ }
210
+ }
211
+ }
212
+ });
213
+ await Page.setLifecycleEventsEnabled({ enabled: false });
214
+ if (options.browserWaitDelay) {
215
+ setTimeout(() => resolve, options.browserWaitDelay);
216
+ }
217
+ }
218
+
219
+ async function waitForDebuggerReady({ Debugger }) {
220
+ await Debugger.enable();
221
+ await Debugger.pause();
222
+ await new Promise(resolve => {
223
+ const RESUMED_EVENT = "resumed";
224
+ Debugger.addEventListener(RESUMED_EVENT, onResumed);
225
+
226
+ function onResumed() {
227
+ Debugger.removeEventListener(RESUMED_EVENT, onResumed);
228
+ resolve();
229
+ }
230
+ });
231
+ await Debugger.disable();
232
+ }
233
+
215
234
  function getBrowserOptions(options) {
216
235
  const browserOptions = {};
217
236
  browserOptions.args = options.browserArgs;
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "2.0.17";
1
+ export const version = "2.0.19";
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.17",
3
+ "version": "2.0.19",
4
4
  "description": "SingleFile CLI",
5
5
  "author": "Gildas Lormeau",
6
6
  "engines": {