single-file-cli 2.0.19 → 2.0.21

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
 
package/deno.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@single-file/single-file-cli",
3
- "version": "2.0.19",
3
+ "version": "2.0.21",
4
4
  "description": "SingleFile CLI",
5
5
  "nodeModulesDir": true,
6
6
  "exports": {
7
7
  ".": "./single-file-cli-api.js"
8
8
  },
9
9
  "imports": {
10
- "simple-cdp": "jsr:@simple-cdp/simple-cdp@^1.8.2",
10
+ "simple-cdp": "jsr:@simple-cdp/simple-cdp@^1.8.4",
11
11
  "path": "jsr:@std/path@^0.220.1"
12
12
  }
13
13
  }
package/deno.lock CHANGED
@@ -2,15 +2,15 @@
2
2
  "version": "3",
3
3
  "packages": {
4
4
  "specifiers": {
5
- "jsr:@simple-cdp/simple-cdp@^1.8.2": "jsr:@simple-cdp/simple-cdp@1.8.2",
5
+ "jsr:@simple-cdp/simple-cdp@^1.8.4": "jsr:@simple-cdp/simple-cdp@1.8.4",
6
6
  "jsr:@std/assert@^0.220.1": "jsr:@std/assert@0.220.1",
7
7
  "jsr:@std/path@^0.220.1": "jsr:@std/path@0.220.1",
8
- "npm:simple-cdp@^1.8.2": "npm:simple-cdp@1.8.2",
8
+ "npm:simple-cdp@^1.8.4": "npm:simple-cdp@1.8.4",
9
9
  "npm:ws@^8.16.0": "npm:ws@8.16.0"
10
10
  },
11
11
  "jsr": {
12
- "@simple-cdp/simple-cdp@1.8.2": {
13
- "integrity": "171f82ef24c464c83f0ffc1148096a04fcd8256c6bb6b5454640614c53689165"
12
+ "@simple-cdp/simple-cdp@1.8.4": {
13
+ "integrity": "5e31ab90a552f9f1cf84e6cf0454d21c5b9dc1d89f9c1f9a750772b47bad9a4e"
14
14
  },
15
15
  "@std/assert@0.220.1": {
16
16
  "integrity": "88710d54f3afdd7a5761e7805abba1f56cd14e4b212feffeb3e73a9f77482425"
@@ -23,8 +23,8 @@
23
23
  }
24
24
  },
25
25
  "npm": {
26
- "simple-cdp@1.8.2": {
27
- "integrity": "sha512-Ts4RpMMuJjdp+mLB2OJeJSUyTJZw3tUDc7ZWKytK6/4zSegwzKJEIqKv0Z2UgX0DPvma0eYg10MfaceM6CYQfA==",
26
+ "simple-cdp@1.8.4": {
27
+ "integrity": "sha512-fsvMUyz13vzX8nxSPwJfdxdROGxRDdTAFX7XitVpRsKwClowsyL3NZ6kR4vR5tZfxbz31p3uoM14eFUvhaWisA==",
28
28
  "dependencies": {}
29
29
  },
30
30
  "ws@8.16.0": {
@@ -36,12 +36,12 @@
36
36
  "remote": {},
37
37
  "workspace": {
38
38
  "dependencies": [
39
- "jsr:@simple-cdp/simple-cdp@^1.8.2",
39
+ "jsr:@simple-cdp/simple-cdp@^1.8.4",
40
40
  "jsr:@std/path@^0.220.1"
41
41
  ],
42
42
  "packageJson": {
43
43
  "dependencies": [
44
- "npm:simple-cdp@^1.8.2",
44
+ "npm:simple-cdp@^1.8.4",
45
45
  "npm:ws@^8.16.0"
46
46
  ]
47
47
  }
package/lib/browser.js CHANGED
@@ -66,13 +66,7 @@ async function launchBrowser(options = {}, indexPath = 0) {
66
66
  if (indexPath + 1 < BROWSER_PATHS[build.os].length) {
67
67
  await launchBrowser(options, indexPath + 1);
68
68
  } else {
69
- let message = "Chromium executable not found. ";
70
- if (options.executablePath) {
71
- message += "Make sure --browser-executable-path is correct.";
72
- } else {
73
- message += "Set the path using the --browser-executable-path option.";
74
- }
75
- throw new Error(message);
69
+ throw error;
76
70
  }
77
71
  } else {
78
72
  throw error;
package/lib/cdp-client.js CHANGED
@@ -37,13 +37,17 @@ const EMPTY_PAGE_URL = "about:blank";
37
37
  export { initialize, getPageData, closeBrowser };
38
38
 
39
39
  async function initialize(options) {
40
- await launchBrowser(getBrowserOptions(options));
40
+ if (!options.browserRemoteDebuggingUrl) {
41
+ await launchBrowser(getBrowserOptions(options));
42
+ } else {
43
+ CDP.remoteDebuggingUrl = options.browserRemoteDebuggingUrl;
44
+ }
41
45
  }
42
46
 
43
47
  async function getPageData(options) {
44
48
  let targetInfo;
45
49
  try {
46
- const targetInfo = await CDP.createTarget(EMPTY_PAGE_URL);
50
+ targetInfo = await CDP.createTarget(EMPTY_PAGE_URL);
47
51
  const { Browser, Security, Page, Emulation, Fetch, Network, Runtime, Debugger } = new CDP(targetInfo);
48
52
  if (options.browserStartMinimized) {
49
53
  const { windowId, bounds } = await Browser.getWindowForTarget({ targetId: targetInfo.targetId });
@@ -96,16 +100,22 @@ async function getPageData(options) {
96
100
  worldName: SINGLE_FILE_WORLD_NAME
97
101
  });
98
102
  const [contextId] = await Promise.all([
99
- navigate({ Page, Runtime }, options),
103
+ loadPage({ Page, Runtime }, options),
100
104
  options.browserDebug ? waitForDebuggerReady({ Debugger }) : Promise.resolve()
101
105
  ]);
106
+ if (options.browserWaitDelay) {
107
+ setTimeout(() => resolve, options.browserWaitDelay);
108
+ }
102
109
  const { result } = await Runtime.evaluate({
103
110
  expression: `singlefile.getPageData(${JSON.stringify(options)})`,
104
111
  awaitPromise: true,
105
112
  returnByValue: true,
106
113
  contextId
107
114
  });
108
- const { value } = result;
115
+ const { value, subtype, description } = result;
116
+ if (subtype === "error") {
117
+ throw new Error(description);
118
+ }
109
119
  if (options.compressContent) {
110
120
  value.content = new Uint8Array(value.content);
111
121
  }
@@ -121,12 +131,12 @@ async function getPageData(options) {
121
131
  throw error;
122
132
  } finally {
123
133
  if (targetInfo && !options.browserDebug) {
124
- await CDP.closeTarget(targetInfo.targetId);
134
+ await CDP.closeTarget(targetInfo.id);
125
135
  }
126
136
  }
127
137
  }
128
138
 
129
- async function navigate({ Page, Runtime }, options) {
139
+ async function loadPage({ Page, Runtime }, options) {
130
140
  await Runtime.enable();
131
141
  await Page.enable();
132
142
  try {
@@ -134,7 +144,7 @@ async function navigate({ Page, Runtime }, options) {
134
144
  const loadTimeoutAbortSignal = loadTimeoutAbortController.signal;
135
145
  const [contextId] = await Promise.race([
136
146
  Promise.all([getTopFrameContextId({ Page, Runtime }, options), Page.navigate({ url: options.url })]),
137
- options.browserLoadMaxTime ? waitForLoadTimeout(loadTimeoutAbortSignal, options.browserLoadMaxTime) : Promise.resolve()
147
+ waitForLoadTimeout(loadTimeoutAbortSignal, options.browserLoadMaxTime)
138
148
  ]);
139
149
  if (!loadTimeoutAbortSignal.aborted) {
140
150
  loadTimeoutAbortController.abort();
@@ -146,34 +156,83 @@ async function navigate({ Page, Runtime }, options) {
146
156
  }
147
157
  }
148
158
 
149
- async function getTopFrameContextId({ Page, Runtime }, options) {
150
- let frameId, contextId;
151
- await new Promise(resolve => {
159
+ function getTopFrameContextId({ Page, Runtime }, options) {
160
+ return new Promise((resolve, reject) => {
152
161
  const FRAME_NAVIGATED_EVENT = "frameNavigated";
153
162
  const CONTEXT_CREATED_EVENT = "executionContextCreated";
163
+ const CONTEXT_DESTROYED_EVENT = "executionContextDestroyed";
164
+ const contextIds = [];
165
+ let frameId, navigationAbortController;
154
166
  Page.addEventListener(FRAME_NAVIGATED_EVENT, onFrameNavigated);
155
- Runtime.addEventListener(CONTEXT_CREATED_EVENT, executionContextCreated);
167
+ Runtime.addEventListener(CONTEXT_CREATED_EVENT, onExecutionContextCreated);
168
+ Runtime.addEventListener(CONTEXT_DESTROYED_EVENT, onExecutionContextDestroyed);
156
169
 
157
170
  async function onFrameNavigated({ params }) {
158
171
  const { frame } = params;
159
172
  if (!frame.parentId) {
160
173
  frameId = frame.id;
161
- await waitForPageReady({ Page }, frameId, options);
174
+ if (navigationAbortController) {
175
+ navigationAbortController.abort();
176
+ contextIds.length = 0;
177
+ }
178
+ navigationAbortController = new AbortController();
179
+ await onTopFrameNavigated(navigationAbortController.signal);
180
+ }
181
+ }
182
+
183
+ async function onTopFrameNavigated(navigationSignal) {
184
+ await waitForPageReady({ Page }, frameId, options, navigationSignal);
185
+ if (!navigationSignal.aborted) {
162
186
  Page.removeEventListener(FRAME_NAVIGATED_EVENT, onFrameNavigated);
163
- Runtime.removeEventListener(CONTEXT_CREATED_EVENT, executionContextCreated);
164
- resolve();
187
+ Runtime.removeEventListener(CONTEXT_CREATED_EVENT, onExecutionContextCreated);
188
+ Runtime.removeEventListener(CONTEXT_DESTROYED_EVENT, onExecutionContextDestroyed);
189
+ let contextId;
190
+ if (contextIds.length) {
191
+ let contextIdIndex = 0;
192
+ do {
193
+ if (await testExecutionContext(contextIds[contextIdIndex])) {
194
+ contextId = contextIds[contextIdIndex];
195
+ }
196
+ contextIdIndex++;
197
+ } while (contextId === undefined && contextIdIndex < contextIds.length);
198
+ }
199
+ if (contextId === undefined) {
200
+ reject(new Error("Execution context not found"));
201
+ } else {
202
+ resolve(contextId);
203
+ }
165
204
  }
166
205
  }
167
206
 
168
- function executionContextCreated({ params }) {
207
+ function onExecutionContextCreated({ params }) {
169
208
  const { context } = params;
170
209
  const { name, origin, auxData } = context;
171
210
  if (frameId !== undefined && origin === "://" && name === SINGLE_FILE_WORLD_NAME && auxData && auxData.frameId === frameId) {
172
- contextId = context.id;
211
+ contextIds.push(context.id);
212
+ }
213
+ }
214
+
215
+ function onExecutionContextDestroyed({ params }) {
216
+ const { executionContextId } = params;
217
+ const index = contextIds.indexOf(executionContextId);
218
+ if (index > -1) {
219
+ contextIds.splice(index, 1);
220
+ }
221
+ }
222
+
223
+ async function testExecutionContext(contextId) {
224
+ try {
225
+ const { result } = await Runtime.evaluate({
226
+ expression: "singlefile !== undefined",
227
+ contextId
228
+ });
229
+ return result.value === true;
230
+ } catch (error) {
231
+ // ignored
173
232
  }
233
+ return false;
174
234
  }
175
235
  });
176
- return contextId;
177
236
  }
178
237
 
179
238
  function waitForLoadTimeout(abortSignal, maxDelay) {
@@ -194,41 +253,46 @@ function waitForLoadTimeout(abortSignal, maxDelay) {
194
253
  });
195
254
  }
196
255
 
197
- async function waitForPageReady({ Page }, topFrameId, options) {
256
+ async function waitForPageReady({ Page }, topFrameId, options, navigationSignal) {
198
257
  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();
258
+ try {
259
+ await new Promise(resolve => {
260
+ const LIFE_CYCLE_EVENT = "lifecycleEvent";
261
+ Page.addEventListener(LIFE_CYCLE_EVENT, onLifecycleEvent);
262
+
263
+ function onLifecycleEvent({ params }) {
264
+ const { name, frameId } = params;
265
+ if (frameId === topFrameId) {
266
+ if (name === options.browserWaitUntil || name === INTERACTIVE_TIME_STATE || navigationSignal.aborted) {
267
+ Page.removeEventListener(LIFE_CYCLE_EVENT, onLifecycleEvent);
268
+ resolve();
269
+ }
209
270
  }
210
271
  }
272
+ });
273
+ } finally {
274
+ if (!navigationSignal.aborted) {
275
+ await Page.setLifecycleEventsEnabled({ enabled: false });
211
276
  }
212
- });
213
- await Page.setLifecycleEventsEnabled({ enabled: false });
214
- if (options.browserWaitDelay) {
215
- setTimeout(() => resolve, options.browserWaitDelay);
216
277
  }
217
278
  }
218
279
 
219
280
  async function waitForDebuggerReady({ Debugger }) {
220
281
  await Debugger.enable();
221
- await Debugger.pause();
222
- await new Promise(resolve => {
223
- const RESUMED_EVENT = "resumed";
224
- Debugger.addEventListener(RESUMED_EVENT, onResumed);
282
+ try {
283
+ await Debugger.pause();
284
+ await new Promise(resolve => {
285
+ const RESUMED_EVENT = "resumed";
286
+ Debugger.addEventListener(RESUMED_EVENT, onResumed);
225
287
 
226
- function onResumed() {
227
- Debugger.removeEventListener(RESUMED_EVENT, onResumed);
228
- resolve();
229
- }
230
- });
231
- await Debugger.disable();
288
+ function onResumed() {
289
+ Debugger.removeEventListener(RESUMED_EVENT, onResumed);
290
+ resolve();
291
+ }
292
+ });
293
+ } finally {
294
+ await Debugger.disable();
295
+ }
232
296
  }
233
297
 
234
298
  function getBrowserOptions(options) {
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "2.0.19";
1
+ export const version = "2.0.21";
package/options.js CHANGED
@@ -60,6 +60,7 @@ const OPTIONS_INFO = {
60
60
  "browser-cookie": { description: "Ordered list of cookie parameters separated by a comma (name,value,domain,path,expires,httpOnly,secure,sameSite,url)", type: "string[]" },
61
61
  "browser-cookies-file": { description: "Path of the cookies file formatted as a JSON file or a Netscape text file", type: "string" },
62
62
  "browser-ignore-insecure-certs": { description: "Ignore HTTPs errors", type: "boolean" },
63
+ "browser-remote-debugging-url": { description: "Remote debugging URL", type: "string" },
63
64
  "compress-content": { description: "Compress the output file into a ZIP file", type: "boolean" },
64
65
  "compress-CSS": { description: "Compress CSS stylesheets", type: "boolean" },
65
66
  "compress-HTML": { description: "Compress HTML content", type: "boolean", defaultValue: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-cli",
3
- "version": "2.0.19",
3
+ "version": "2.0.21",
4
4
  "description": "SingleFile CLI",
5
5
  "author": "Gildas Lormeau",
6
6
  "engines": {
@@ -13,7 +13,7 @@
13
13
  "single-file": "./single-file-node.js"
14
14
  },
15
15
  "dependencies": {
16
- "simple-cdp": "^1.8.2",
16
+ "simple-cdp": "^1.8.4",
17
17
  "ws": "^8.16.0"
18
18
  }
19
19
  }
@@ -76,7 +76,7 @@ const DEFAULT_OPTIONS = {
76
76
  const STATE_PROCESSING = "processing";
77
77
  const STATE_PROCESSED = "processed";
78
78
 
79
- const { readTextFile, writeTextFile, writeFile, stdout, mkdir, stat } = Deno;
79
+ const { readTextFile, writeTextFile, writeFile, stdout, mkdir, stat, errors } = Deno;
80
80
  let tasks = [], maxParallelWorkers, sessionFilename;
81
81
 
82
82
  export { DEFAULT_OPTIONS, VALID_URL_TEST, initialize };
@@ -84,7 +84,21 @@ export { DEFAULT_OPTIONS, VALID_URL_TEST, initialize };
84
84
  async function initialize(options) {
85
85
  options = Object.assign({}, DEFAULT_OPTIONS, options);
86
86
  maxParallelWorkers = options.maxParallelWorkers || 8;
87
- await backend.initialize(options);
87
+ try {
88
+ await backend.initialize(options);
89
+ } catch (error) {
90
+ if (error instanceof errors.NotFound) {
91
+ let message = "Chromium executable not found. ";
92
+ if (options.browserExecutablePath) {
93
+ message += "Make sure --browser-executable-path is correct.";
94
+ } else {
95
+ message += "Set the path using the --browser-executable-path option.";
96
+ }
97
+ throw new Error(message);
98
+ } else {
99
+ throw error;
100
+ }
101
+ }
88
102
  if (options.crawlSyncSession || options.crawlLoadSession) {
89
103
  try {
90
104
  tasks = JSON.parse(await readTextFile(options.crawlSyncSession || options.crawlLoadSession));