single-file-cli 2.1.0 → 2.1.2

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/compile.sh CHANGED
@@ -9,15 +9,14 @@ trap 'rm -rf "$stage_dir"' EXIT
9
9
  cp ./single-file ./single-file-launcher.js ./single-file-cli-api.js ./options.js ./deno.json "$stage_dir"
10
10
  cp -R ./lib "$stage_dir/lib"
11
11
  cp -R ./resources "$stage_dir/resources"
12
- jq 'del(.dependencies, .devDependencies)' ./package.json > "$stage_dir/package.json"
13
12
  mkdir -p "$dist_dir"
14
13
  cd "$stage_dir"
15
14
 
16
- deno compile --allow-read --allow-write --allow-net --allow-env --allow-run --ext=js --output="$dist_dir/single-file-aarch64-apple-darwin" --target=aarch64-apple-darwin ./single-file
17
- deno compile --allow-read --allow-write --allow-net --allow-env --allow-run --ext=js --output="$dist_dir/single-file-x86_64-apple-darwin" --target=x86_64-apple-darwin ./single-file
18
- deno compile --allow-read --allow-write --allow-net --allow-env --allow-run --ext=js --output="$dist_dir/single-file-x86_64-linux" --target=x86_64-unknown-linux-gnu ./single-file
19
- deno compile --allow-read --allow-write --allow-net --allow-env --allow-run --ext=js --output="$dist_dir/single-file-aarch64-linux" --target=aarch64-unknown-linux-gnu ./single-file
20
- deno compile --allow-all --ext=js --output="$dist_dir/single-file.exe" --target=x86_64-pc-windows-msvc --icon=./resources/single-file.ico ./single-file
15
+ deno compile --minimum-dependency-age=0 --allow-read --allow-write --allow-net --allow-env --allow-run --ext=js --output="$dist_dir/single-file-aarch64-apple-darwin" --target=aarch64-apple-darwin ./single-file
16
+ deno compile --minimum-dependency-age=0 --allow-read --allow-write --allow-net --allow-env --allow-run --ext=js --output="$dist_dir/single-file-x86_64-apple-darwin" --target=x86_64-apple-darwin ./single-file
17
+ deno compile --minimum-dependency-age=0 --allow-read --allow-write --allow-net --allow-env --allow-run --ext=js --output="$dist_dir/single-file-x86_64-linux" --target=x86_64-unknown-linux-gnu ./single-file
18
+ deno compile --minimum-dependency-age=0 --allow-read --allow-write --allow-net --allow-env --allow-run --ext=js --output="$dist_dir/single-file-aarch64-linux" --target=aarch64-unknown-linux-gnu ./single-file
19
+ deno compile --minimum-dependency-age=0 --allow-all --ext=js --output="$dist_dir/single-file.exe" --target=x86_64-pc-windows-msvc --icon=./resources/single-file.ico ./single-file
21
20
 
22
21
  cd - > /dev/null
23
22
 
package/deno.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@single-file/single-file-cli",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "SingleFile CLI",
5
5
  "exports": {
6
6
  ".": "./single-file-cli-api.js"
7
7
  },
8
8
  "imports": {
9
- "simple-cdp": "jsr:@simple-cdp/simple-cdp@^1.10.0",
10
- "path": "jsr:@std/path@^1.1.2"
9
+ "simple-cdp": "jsr:@simple-cdp/simple-cdp@^1.10.1",
10
+ "path": "jsr:@std/path@^1.1.6"
11
11
  }
12
12
  }
package/deno.lock CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "version": "5",
3
3
  "specifiers": {
4
- "jsr:@simple-cdp/simple-cdp@^1.10.0": "1.10.0",
4
+ "jsr:@simple-cdp/simple-cdp@^1.10.1": "1.10.1",
5
5
  "jsr:@std/internal@^1.0.14": "1.0.14",
6
- "jsr:@std/path@^1.1.2": "1.1.6",
7
- "npm:@eslint/js@^9.39.1": "9.39.5",
8
- "npm:esbuild@~0.27.4": "0.27.7",
6
+ "jsr:@std/path@^1.1.6": "1.1.6",
7
+ "npm:@eslint/js@^9.39.5": "9.39.5",
8
+ "npm:esbuild@~0.27.7": "0.27.7",
9
9
  "npm:eslint@^10.8.1": "10.8.1",
10
- "npm:simple-cdp@^1.10.0": "1.10.0"
10
+ "npm:simple-cdp@^1.10.1": "1.10.1"
11
11
  },
12
12
  "jsr": {
13
- "@simple-cdp/simple-cdp@1.10.0": {
14
- "integrity": "3f8c91e22598c3e15a680f14ee3d2aa174c21b9b528caad5ec8552e3df0c3897"
13
+ "@simple-cdp/simple-cdp@1.10.1": {
14
+ "integrity": "33c686242e812a53d13ba7fcebd18ed81b4685fb02da37c2ee6ed9a5123f9b8c"
15
15
  },
16
16
  "@std/internal@1.0.14": {
17
17
  "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7"
@@ -527,8 +527,8 @@
527
527
  "shebang-regex@3.0.0": {
528
528
  "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
529
529
  },
530
- "simple-cdp@1.10.0": {
531
- "integrity": "sha512-UXDAaiges1GqNKtBy39po+CD4hXDJxQEDtR1GF9bK2J9MNN0epTo2GpoSmEc8o5ofW8dutpHafBjowEchSNgig=="
530
+ "simple-cdp@1.10.1": {
531
+ "integrity": "sha512-svGX76zc2MY2ErGAWGb9NWP9aF5XQgDyx00iqAGCgRi1qQd5MQjtBwuC7mcktliFH263gpHdnHicfsB+n7Vnyg=="
532
532
  },
533
533
  "type-check@0.4.0": {
534
534
  "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
@@ -558,15 +558,15 @@
558
558
  },
559
559
  "workspace": {
560
560
  "dependencies": [
561
- "jsr:@simple-cdp/simple-cdp@^1.10.0",
562
- "jsr:@std/path@^1.1.2"
561
+ "jsr:@simple-cdp/simple-cdp@^1.10.1",
562
+ "jsr:@std/path@^1.1.6"
563
563
  ],
564
564
  "packageJson": {
565
565
  "dependencies": [
566
- "npm:@eslint/js@^9.39.1",
567
- "npm:esbuild@~0.27.4",
566
+ "npm:@eslint/js@^9.39.5",
567
+ "npm:esbuild@~0.27.7",
568
568
  "npm:eslint@^10.8.1",
569
- "npm:simple-cdp@^1.10.0"
569
+ "npm:simple-cdp@^1.10.1"
570
570
  ]
571
571
  }
572
572
  }
package/lib/browser.js CHANGED
@@ -21,7 +21,7 @@
21
21
  * Source.
22
22
  */
23
23
 
24
- /* global fetch */
24
+ /* global fetch, setTimeout */
25
25
 
26
26
  import { BROWSER_PATHS, BROWSER_ARGS } from "./constants.js";
27
27
  import { Deno } from "./deno-polyfill.js";
@@ -29,10 +29,12 @@ import { Deno } from "./deno-polyfill.js";
29
29
  const NULL_STD_CONFIG = "null";
30
30
  const DEBUG_PORT_MIN = 9222;
31
31
  const DEBUG_PORT_RANGE = 256;
32
+ const READY_TIMEOUT = 30000;
33
+ const READY_RETRY_DELAY = 250;
32
34
 
33
35
  const { build, makeTempDir, Command, errors, remove } = Deno;
34
- let child, profilePath;
35
- export { launchBrowser, closeBrowser };
36
+ let child, profilePath, childExited;
37
+ export { launchBrowser, closeBrowser, browserExited };
36
38
 
37
39
  async function launchBrowser(options = {}, indexPath = 0) {
38
40
  const executablePath = options.executablePath || BROWSER_PATHS[build.os][indexPath];
@@ -78,15 +80,58 @@ async function launchBrowser(options = {}, indexPath = 0) {
78
80
  } catch (error) {
79
81
  await remove(profilePath, { recursive: true }).catch(() => { });
80
82
  profilePath = undefined;
81
- if (error instanceof errors.NotFound && indexPath + 1 < BROWSER_PATHS[build.os].length) {
82
- return launchBrowser(options, indexPath + 1);
83
+ if (error instanceof errors.NotFound) {
84
+ if (!options.executablePath && indexPath + 1 < BROWSER_PATHS[build.os].length) {
85
+ return launchBrowser(options, indexPath + 1);
86
+ }
87
+ throw new Error(options.executablePath ?
88
+ `The browser executable was not found at ${JSON.stringify(executablePath)}` :
89
+ "The browser executable was not found, use --browser-executable-path to set its location");
83
90
  }
84
91
  throw error;
85
92
  }
86
93
  child.ref();
94
+ childExited = false;
95
+ child.status.then(() => childExited = true);
96
+ try {
97
+ await waitUntilReady(debugPort);
98
+ } catch (error) {
99
+ await closeBrowser();
100
+ if (options.singleProcess) {
101
+ console.warn("Warning: the browser exited when using --browser-single-process, retrying without it"); // eslint-disable-line no-console
102
+ return launchBrowser(Object.assign({}, options, { singleProcess: false }), indexPath);
103
+ }
104
+ throw error;
105
+ }
87
106
  return debugPort;
88
107
  }
89
108
 
109
+ async function waitUntilReady(debugPort) {
110
+ const timeoutTime = Date.now() + READY_TIMEOUT;
111
+ while (!childExited && Date.now() < timeoutTime) {
112
+ try {
113
+ await fetch("http://localhost:" + debugPort + "/json/version");
114
+ return;
115
+ } catch {
116
+ await new Promise(resolve => setTimeout(resolve, READY_RETRY_DELAY));
117
+ }
118
+ }
119
+ throw new Error(childExited ? "The browser exited unexpectedly" : "The browser is not responding");
120
+ }
121
+
122
+ async function browserExited(maxDelay = 0) {
123
+ if (child === undefined) {
124
+ return false;
125
+ }
126
+ if (childExited || !maxDelay) {
127
+ return childExited;
128
+ }
129
+ return Promise.race([
130
+ child.status.then(() => true),
131
+ new Promise(resolve => setTimeout(() => resolve(childExited), maxDelay))
132
+ ]);
133
+ }
134
+
90
135
  async function getDebugPort(port = getRandomDebugPort(), usedPorts = []) {
91
136
  try {
92
137
  await fetch("http://localhost:" + port + "/json/version");
@@ -111,7 +156,11 @@ function getRandomDebugPort() {
111
156
 
112
157
  async function closeBrowser() {
113
158
  if (child !== undefined) {
114
- child.kill();
159
+ try {
160
+ child.kill();
161
+ } catch {
162
+ // ignored
163
+ }
115
164
  await child.status;
116
165
  child = undefined;
117
166
  }
@@ -23,6 +23,7 @@
23
23
 
24
24
  /* global setTimeout, clearTimeout, fetch, URL, Headers */
25
25
 
26
+ import { Buffer } from "node:buffer";
26
27
  import { Deno, isDeno, path } from "./deno-polyfill.js";
27
28
 
28
29
  const ABORT_EVENT = "abort";
@@ -82,7 +83,7 @@ function waitForTimeout(abortSignal, maxDelay, errorMessage, errorCode) {
82
83
  }
83
84
 
84
85
  function arrayBufferToBase64(arrayBuffer) {
85
- return new Uint8Array(arrayBuffer).toBase64();
86
+ return Buffer.from(arrayBuffer).toString("base64");
86
87
  }
87
88
 
88
89
  function getAlternativeUrl(url) {
package/lib/cdp-client.js CHANGED
@@ -25,7 +25,8 @@
25
25
 
26
26
  import {
27
27
  launchBrowser,
28
- closeBrowser
28
+ closeBrowser,
29
+ browserExited
29
30
  } from "./browser.js";
30
31
  import {
31
32
  CDP,
@@ -57,6 +58,10 @@ const SET_SCREENSHOT_FUNCTION_NAME = "setScreenshot";
57
58
  const SET_PDF_FUNCTION_NAME = "setPDF";
58
59
  const SET_PAGE_DATA_FUNCTION_NAME = "setPageData";
59
60
  const BINDING_CALLED_EVENT_TYPE = "bindingCalled";
61
+ const LOCALHOST = "http://localhost:";
62
+ const BROWSER_EXITED_MAX_DELAY = 2000;
63
+
64
+ let browserOptions, relaunchBrowserPromise;
60
65
 
61
66
  export {
62
67
  initialize,
@@ -68,8 +73,7 @@ async function initialize(singleFileOptions) {
68
73
  if (singleFileOptions.browserServer) {
69
74
  options.apiUrl = singleFileOptions.browserServer;
70
75
  } else {
71
- const LOCALHOST = "http://localhost:";
72
- const browserOptions = {};
76
+ browserOptions = {};
73
77
  browserOptions.args = singleFileOptions.browserArgs;
74
78
  browserOptions.headless = singleFileOptions.browserHeadless;
75
79
  browserOptions.executablePath = singleFileOptions.browserExecutablePath;
@@ -84,9 +88,21 @@ async function initialize(singleFileOptions) {
84
88
  }
85
89
  }
86
90
 
91
+ function relaunchBrowser() {
92
+ if (!relaunchBrowserPromise) {
93
+ console.warn("Warning: the browser exited when using --browser-single-process, retrying without it"); // eslint-disable-line no-console
94
+ relaunchBrowserPromise = (async () => {
95
+ await closeBrowser();
96
+ browserOptions.singleProcess = false;
97
+ options.apiUrl = LOCALHOST + (await launchBrowser(browserOptions));
98
+ })();
99
+ }
100
+ return relaunchBrowserPromise;
101
+ }
102
+
87
103
  async function getPageData(options) {
88
104
  const EMPTY_PAGE_URL = "about:blank";
89
- const pageContext = { options, consoleMessages: [], debugMessages: [], httpInfo: {} };
105
+ const pageContext = { options, consoleMessages: [], debugMessages: [], httpInfo: {}, fetchAbortController: new AbortController() };
90
106
  let targetInfo, cdp;
91
107
  try {
92
108
  logData(["Loading page", EMPTY_PAGE_URL], pageContext);
@@ -108,10 +124,14 @@ async function getPageData(options) {
108
124
  if (shouldRetryWithFallback(error)) {
109
125
  return await retryWithFallback();
110
126
  }
127
+ if (await shouldRelaunchBrowser()) {
128
+ return await relaunchBrowserAndRetry();
129
+ }
111
130
  attachDebugInfo(error, pageContext);
112
131
  throw error;
113
132
  } finally {
114
133
  logData(["Closing page"], pageContext);
134
+ pageContext.fetchAbortController.abort();
115
135
  await closeTarget();
116
136
  logData(["Finishing"], pageContext);
117
137
  }
@@ -131,9 +151,28 @@ async function getPageData(options) {
131
151
  return await getPageData(options);
132
152
  }
133
153
 
154
+ async function shouldRelaunchBrowser() {
155
+ return Boolean(browserOptions && browserOptions.singleProcess) && await browserExited(BROWSER_EXITED_MAX_DELAY);
156
+ }
157
+
158
+ async function relaunchBrowserAndRetry() {
159
+ logData(["Relaunching the browser"], pageContext);
160
+ targetInfo = null;
161
+ if (cdp) {
162
+ cdp.reset();
163
+ cdp = null;
164
+ }
165
+ await relaunchBrowser();
166
+ return await getPageData(options);
167
+ }
168
+
134
169
  async function closeTarget() {
135
170
  if (targetInfo && !options.browserDebug) {
136
- await CDP.closeTarget(targetInfo.id);
171
+ try {
172
+ await CDP.closeTarget(targetInfo.id);
173
+ } catch {
174
+ // ignored
175
+ }
137
176
  targetInfo = null;
138
177
  }
139
178
  // the connection is closed even when the target is left open for
@@ -416,14 +455,16 @@ async function loadPage({ Page, Runtime }, { options, debugMessages }) {
416
455
  ]),
417
456
  waitForTimeout(loadTimeoutAbortSignal, options.browserLoadMaxTime, LOAD_TIMEOUT_ERROR_MESSAGE, LOAD_TIMEOUT_ERROR)
418
457
  ]);
458
+ // only called when the page loaded, these calls can hang forever on an
459
+ // unresponsive page and would mask the load timeout error
460
+ await Page.setLifecycleEventsEnabled({ enabled: false });
461
+ await Runtime.disable();
462
+ await Page.disable();
419
463
  return contextId;
420
464
  } finally {
421
465
  if (!loadTimeoutAbortSignal.aborted) {
422
466
  loadTimeoutAbortController.abort();
423
467
  }
424
- await Page.setLifecycleEventsEnabled({ enabled: false });
425
- await Runtime.disable();
426
- await Page.disable();
427
468
  }
428
469
  }
429
470
 
@@ -533,7 +574,7 @@ function setupPageDataCapture({ Runtime }, _contextId, { options, debugMessages
533
574
  });
534
575
  }
535
576
 
536
- async function setupBindings({ Page, Runtime }, contextId, { options, debugMessages }) {
577
+ async function setupBindings({ Page, Runtime }, contextId, { options, debugMessages, fetchAbortController }) {
537
578
  await Runtime.addBinding({ name: SET_PAGE_DATA_FUNCTION_NAME, executionContextId: contextId });
538
579
  if (options.embedScreenshot && options.compressContent) {
539
580
  await setupScreenshotCapture({ Page, Runtime }, contextId, { options, debugMessages });
@@ -544,7 +585,7 @@ async function setupBindings({ Page, Runtime }, contextId, { options, debugMessa
544
585
  await Runtime.addBinding({ name: FETCH_FUNCTION_NAME, executionContextId: contextId });
545
586
  Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ignoringErrors(async ({ params }) => {
546
587
  if (params.name === FETCH_FUNCTION_NAME) {
547
- await handleFetchRequest({ Runtime }, params, contextId, { options, debugMessages });
588
+ await handleFetchRequest({ Runtime }, params, contextId, { options, debugMessages, fetchAbortController });
548
589
  }
549
590
  }, { options, debugMessages }));
550
591
  }
@@ -607,12 +648,12 @@ async function setupPdfCapture({ Page, Runtime }, contextId, { options, debugMes
607
648
  }
608
649
  }
609
650
 
610
- async function handleFetchRequest({ Runtime }, params, contextId, { options, debugMessages }) {
651
+ async function handleFetchRequest({ Runtime }, params, contextId, { options, debugMessages, fetchAbortController }) {
611
652
  const { payload } = params;
612
653
  const { requestId, url, options: fetchOptions } = JSON.parse(payload);
613
654
  logData(["Fetching URL", url], { options, debugMessages });
614
655
  try {
615
- const response = await fetch(url, fetchOptions);
656
+ const response = await fetch(url, Object.assign({}, fetchOptions, { signal: fetchAbortController.signal }));
616
657
  const arrayBuffer = await response.arrayBuffer();
617
658
  const base64Data = arrayBufferToBase64(arrayBuffer);
618
659
  const result = {
@@ -53,6 +53,12 @@ const errors = {
53
53
  super(message);
54
54
  this.name = "NotFound";
55
55
  }
56
+ },
57
+ AlreadyExists: DENO_RUNTIME_DETECTED ? Deno.errors.AlreadyExists : class AlreadyExists extends Error {
58
+ constructor(message) {
59
+ super(message);
60
+ this.name = "AlreadyExists";
61
+ }
56
62
  }
57
63
  };
58
64
 
@@ -157,7 +163,7 @@ async function writeTextFile(path, data, options = {}) {
157
163
  if (options.append) {
158
164
  return fsPromise.appendFile(path, data, options);
159
165
  } else {
160
- return fsPromise.writeFile(path, data, options);
166
+ return nodeWriteFile(fsPromise, path, data, options);
161
167
  }
162
168
  }
163
169
  }
@@ -167,7 +173,18 @@ async function writeFile(path, data, options = {}) {
167
173
  return Deno.writeFile(path, data, options);
168
174
  } else {
169
175
  const fsPromise = await import(NODE_MODULES["fs"]);
170
- return fsPromise.writeFile(path, data, options);
176
+ return nodeWriteFile(fsPromise, path, data, options);
177
+ }
178
+ }
179
+
180
+ async function nodeWriteFile(fsPromise, path, data, options) {
181
+ if (options.createNew) {
182
+ options.flag = "wx";
183
+ }
184
+ try {
185
+ return await fsPromise.writeFile(path, data, options);
186
+ } catch (error) {
187
+ throw error.code == "EEXIST" ? new errors.AlreadyExists(error.message) : error;
171
188
  }
172
189
  }
173
190
 
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "2.1.0";
1
+ export const version = "2.1.2";