single-file-cli 2.9.1 → 2.9.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/deno.json +1 -1
- package/lib/bidi-client.js +13 -2
- package/lib/cdp-client.js +12 -2
- package/lib/single-file-script.js +6 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/test/e2e/browser-script.test.js +72 -0
package/deno.json
CHANGED
package/lib/bidi-client.js
CHANGED
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
FETCH_FUNCTION_NAME,
|
|
35
35
|
RESOLVE_FETCH_FUNCTION_NAME,
|
|
36
36
|
REJECT_FETCH_FUNCTION_NAME,
|
|
37
|
+
SCRIPT_READY_PROPERTY_NAME,
|
|
38
|
+
SCRIPT_ERROR_PROPERTY_NAME,
|
|
37
39
|
getScriptSource,
|
|
38
40
|
getHookScriptSource,
|
|
39
41
|
getPageDataScriptSource
|
|
@@ -1100,7 +1102,7 @@ function waitForPageReadyState(state, waitUntil, pageContext) {
|
|
|
1100
1102
|
}
|
|
1101
1103
|
|
|
1102
1104
|
async function checkSingleFileContext(pageContext) {
|
|
1103
|
-
const SINGLE_FILE_DETECTION_TEST = "typeof singlefile !== 'undefined'";
|
|
1105
|
+
const SINGLE_FILE_DETECTION_TEST = "typeof singlefile !== 'undefined' && globalThis." + SCRIPT_READY_PROPERTY_NAME + " === true";
|
|
1104
1106
|
const NO_VALID_CONTEXT_ERROR_MESSAGE = "No valid SingleFile execution context found";
|
|
1105
1107
|
const { context } = pageContext;
|
|
1106
1108
|
logData(["Getting execution context"], pageContext);
|
|
@@ -1110,10 +1112,19 @@ async function checkSingleFileContext(pageContext) {
|
|
|
1110
1112
|
awaitPromise: false
|
|
1111
1113
|
});
|
|
1112
1114
|
if (!result || result.value !== true) {
|
|
1113
|
-
throw new Error(NO_VALID_CONTEXT_ERROR_MESSAGE);
|
|
1115
|
+
throw new Error(NO_VALID_CONTEXT_ERROR_MESSAGE + await getInjectionErrorSuffix(context));
|
|
1114
1116
|
}
|
|
1115
1117
|
}
|
|
1116
1118
|
|
|
1119
|
+
async function getInjectionErrorSuffix(context) {
|
|
1120
|
+
const { result } = await session.send("script.evaluate", {
|
|
1121
|
+
expression: "globalThis." + SCRIPT_ERROR_PROPERTY_NAME,
|
|
1122
|
+
target: getSingleFileTarget(context),
|
|
1123
|
+
awaitPromise: false
|
|
1124
|
+
}).catch(() => ({}));
|
|
1125
|
+
return result && result.value ? ": " + result.value : "";
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1117
1128
|
async function capturePageData(pageContext) {
|
|
1118
1129
|
const CAPTURE_TIMEOUT_ERROR_MESSAGE = "Capture timeout";
|
|
1119
1130
|
const EXCEPTION_TYPE = "exception";
|
package/lib/cdp-client.js
CHANGED
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
FETCH_FUNCTION_NAME,
|
|
38
38
|
RESOLVE_FETCH_FUNCTION_NAME,
|
|
39
39
|
REJECT_FETCH_FUNCTION_NAME,
|
|
40
|
+
SCRIPT_READY_PROPERTY_NAME,
|
|
41
|
+
SCRIPT_ERROR_PROPERTY_NAME,
|
|
40
42
|
getScriptSource,
|
|
41
43
|
getHookScriptSource,
|
|
42
44
|
getPageDataScriptSource
|
|
@@ -670,7 +672,7 @@ function createFrameNavigatedHandler(state, timeoutState, reject, cleanup, { opt
|
|
|
670
672
|
}
|
|
671
673
|
|
|
672
674
|
async function getSingleFileContext({ Page, Runtime }, topFrameId, { options, debugMessages }) {
|
|
673
|
-
const SINGLE_FILE_DETECTION_TEST = "typeof singlefile !== 'undefined'";
|
|
675
|
+
const SINGLE_FILE_DETECTION_TEST = "typeof singlefile !== 'undefined' && globalThis." + SCRIPT_READY_PROPERTY_NAME + " === true";
|
|
674
676
|
const NO_VALID_CONTEXT_ERROR_MESSAGE = "No valid SingleFile execution context found";
|
|
675
677
|
logData(["Getting execution context"], { options, debugMessages });
|
|
676
678
|
// the world already exists, so asking for it by name returns the context the
|
|
@@ -687,11 +689,19 @@ async function getSingleFileContext({ Page, Runtime }, topFrameId, { options, de
|
|
|
687
689
|
contextId: executionContextId
|
|
688
690
|
});
|
|
689
691
|
if (result.value !== true) {
|
|
690
|
-
throw new Error(NO_VALID_CONTEXT_ERROR_MESSAGE);
|
|
692
|
+
throw new Error(NO_VALID_CONTEXT_ERROR_MESSAGE + await getInjectionErrorSuffix(Runtime, executionContextId));
|
|
691
693
|
}
|
|
692
694
|
return executionContextId;
|
|
693
695
|
}
|
|
694
696
|
|
|
697
|
+
async function getInjectionErrorSuffix(Runtime, executionContextId) {
|
|
698
|
+
const { result } = await Runtime.evaluate({
|
|
699
|
+
expression: "globalThis." + SCRIPT_ERROR_PROPERTY_NAME,
|
|
700
|
+
contextId: executionContextId
|
|
701
|
+
}).catch(() => ({}));
|
|
702
|
+
return result && result.value ? ": " + result.value : "";
|
|
703
|
+
}
|
|
704
|
+
|
|
695
705
|
function setupPageDataCapture({ Runtime }, { options, debugMessages }) {
|
|
696
706
|
return new Promise((resolve, reject) => {
|
|
697
707
|
let pageDataResponse = "";
|
|
@@ -29,6 +29,8 @@ import { Deno } from "./deno-polyfill.js";
|
|
|
29
29
|
const FETCH_FUNCTION_NAME = "__singleFileFetch";
|
|
30
30
|
const RESOLVE_FETCH_FUNCTION_NAME = "__singleFileResolveFetch";
|
|
31
31
|
const REJECT_FETCH_FUNCTION_NAME = "__singleFileRejectFetch";
|
|
32
|
+
const SCRIPT_READY_PROPERTY_NAME = "__singleFileScriptReady";
|
|
33
|
+
const SCRIPT_ERROR_PROPERTY_NAME = "__singleFileScriptError";
|
|
32
34
|
|
|
33
35
|
const { readTextFile } = Deno;
|
|
34
36
|
|
|
@@ -36,6 +38,8 @@ export {
|
|
|
36
38
|
FETCH_FUNCTION_NAME,
|
|
37
39
|
RESOLVE_FETCH_FUNCTION_NAME,
|
|
38
40
|
REJECT_FETCH_FUNCTION_NAME,
|
|
41
|
+
SCRIPT_READY_PROPERTY_NAME,
|
|
42
|
+
SCRIPT_ERROR_PROPERTY_NAME,
|
|
39
43
|
getScriptSource,
|
|
40
44
|
getHookScriptSource,
|
|
41
45
|
getZipScriptSource,
|
|
@@ -101,7 +105,8 @@ async function getScriptSource(options) {
|
|
|
101
105
|
RESOLVE_FETCH_FUNCTION_NAME,
|
|
102
106
|
REJECT_FETCH_FUNCTION_NAME
|
|
103
107
|
}) + ");";
|
|
104
|
-
return source;
|
|
108
|
+
return "try{" + source + "\nglobalThis." + SCRIPT_READY_PROPERTY_NAME + "=true;}" +
|
|
109
|
+
"catch(error){globalThis." + SCRIPT_ERROR_PROPERTY_NAME + "=String(error);throw error;}";
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
function getHookScriptSource() {
|
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "2.9.
|
|
1
|
+
export const version = "2.9.2";
|
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// The injected script is one concatenation: the SingleFile bundle, then the scripts named by
|
|
2
|
+
// --browser-script, then the call that hands SingleFile its options and its fetch fallback. A
|
|
3
|
+
// throw anywhere in it stops the rest, and nothing reported it — the browser does not surface an
|
|
4
|
+
// exception raised by a preload script, and the only guard was "is `singlefile` defined", which is
|
|
5
|
+
// already true by then. So a script that threw produced a save that looked ordinary and was
|
|
6
|
+
// missing the fetch fallback the CLI installs for the resources the page itself cannot get.
|
|
7
|
+
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import process from "node:process";
|
|
18
|
+
import { cliDirectory } from "../target.js";
|
|
19
|
+
|
|
20
|
+
const execFileAsync = promisify(execFile);
|
|
21
|
+
const TEST_TIMEOUT = 120000;
|
|
22
|
+
|
|
23
|
+
test("a browser script runs in the page it saves", { timeout: TEST_TIMEOUT }, async () => {
|
|
24
|
+
const MARKER = "browser-script-marker";
|
|
25
|
+
await withPage(async ({ url, directory }) => {
|
|
26
|
+
const scriptPath = join(directory, "marker.js");
|
|
27
|
+
await writeFile(scriptPath, "addEventListener(\"DOMContentLoaded\",()=>{" +
|
|
28
|
+
"const marker=document.createElement(\"p\");marker.id=\"" + MARKER + "\";document.body.appendChild(marker);});");
|
|
29
|
+
const outputPath = join(directory, "out.html");
|
|
30
|
+
await execFileAsync(process.execPath, [
|
|
31
|
+
"single-file-node.js", url, outputPath,
|
|
32
|
+
"--browser-script", scriptPath
|
|
33
|
+
], { cwd: cliDirectory });
|
|
34
|
+
assert.match(await readFile(outputPath, "utf8"), new RegExp(MARKER));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("a browser script that throws fails the save instead of degrading it", { timeout: TEST_TIMEOUT }, async () => {
|
|
39
|
+
const SCRIPT_ERROR_MESSAGE = "browser script boom";
|
|
40
|
+
await withPage(async ({ url, directory }) => {
|
|
41
|
+
const scriptPath = join(directory, "throwing.js");
|
|
42
|
+
await writeFile(scriptPath, "throw new Error(\"" + SCRIPT_ERROR_MESSAGE + "\");");
|
|
43
|
+
const outputPath = join(directory, "out.html");
|
|
44
|
+
let exitCode = 0, stderr = "";
|
|
45
|
+
try {
|
|
46
|
+
await execFileAsync(process.execPath, [
|
|
47
|
+
"single-file-node.js", url, outputPath,
|
|
48
|
+
"--browser-script", scriptPath
|
|
49
|
+
], { cwd: cliDirectory });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
({ code: exitCode, stderr } = error);
|
|
52
|
+
}
|
|
53
|
+
assert.notEqual(exitCode, 0, "a save whose injected script threw was reported as a success");
|
|
54
|
+
// the message the script threw is what says which script to look at: without it the
|
|
55
|
+
// failure names the execution context and leaves the cause to be guessed
|
|
56
|
+
assert.match(stderr, new RegExp(SCRIPT_ERROR_MESSAGE), "the failure does not name the error the script threw");
|
|
57
|
+
assert.ok(!existsSync(outputPath), "a page was written for a save that failed");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
async function withPage(run) {
|
|
62
|
+
const server = createServer((request, response) =>
|
|
63
|
+
response.writeHead(200, { "content-type": "text/html" }).end("<html><body><h1>page</h1></body></html>"));
|
|
64
|
+
await new Promise(resolve => server.listen(0, "localhost", resolve));
|
|
65
|
+
const directory = await mkdtemp(join(tmpdir(), "single-file-test-"));
|
|
66
|
+
try {
|
|
67
|
+
await run({ url: "http://localhost:" + server.address().port + "/", directory });
|
|
68
|
+
} finally {
|
|
69
|
+
await rm(directory, { recursive: true, force: true });
|
|
70
|
+
server.close();
|
|
71
|
+
}
|
|
72
|
+
}
|