single-file-cli 2.3.0 → 2.4.0
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 +9 -0
- package/build.sh +4 -5
- package/deno.json +1 -1
- package/dev-build.sh +9 -13
- package/lib/browser.js +150 -17
- package/lib/cdp-client.js +133 -57
- package/lib/constants.js +0 -1
- package/lib/deno-polyfill.js +42 -0
- package/lib/single-file-archive.js +10 -6
- package/lib/single-file-bundle.js +1 -1
- package/lib/version.js +1 -1
- package/options.js +14 -2
- package/package.json +1 -1
- package/single-file-cli-api.js +7 -1
- package/single-file-launcher.js +14 -2
- package/test/e2e/automation-detection.test.js +103 -0
- package/test/e2e/browser-profile.test.js +67 -0
- package/test/e2e/oopif.test.js +86 -0
- package/test/unit/archive-packager.test.js +1 -2
- package/test/unit/browser-profile.test.js +70 -0
- package/test/unit/cli-usage.test.js +15 -0
- package/lib/archive-packager.js +0 -228
package/README.MD
CHANGED
|
@@ -124,6 +124,15 @@ Make sure Chrome or a Chromium-based browser is installed in the default folder.
|
|
|
124
124
|
single-file https://www.wikipedia.org wikipedia.html
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
- Save a page needing a logged-in session. Create the profile first, log in
|
|
128
|
+
to the website in the browser window, and quit the browser to save it. Then
|
|
129
|
+
reuse the profile as many times as needed, it is copied and left unmodified.
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
single-file --create-browser-profile ./profiles/example https://www.example.com/login
|
|
133
|
+
single-file https://www.example.com/account account.html --browser-profile=./profiles/example
|
|
134
|
+
```
|
|
135
|
+
|
|
127
136
|
- Save a list of URLs stored into `list-urls.txt` in the current folder
|
|
128
137
|
|
|
129
138
|
```sh
|
package/build.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
mv package.json package.json.tmp
|
|
4
4
|
mv deno.json deno.json.tmp
|
|
5
5
|
mv deno.lock deno.lock.tmp
|
|
6
|
-
deno install --vendor --quiet --minimum-dependency-age=0 "npm:single-file-core@1.5.
|
|
6
|
+
deno install --vendor --quiet --minimum-dependency-age=0 "npm:single-file-core@1.5.106"
|
|
7
7
|
mv package.json.tmp package.json
|
|
8
8
|
mv deno.json.tmp deno.json
|
|
9
9
|
mv deno.lock.tmp deno.lock
|
|
@@ -67,10 +67,9 @@ await build({
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
await build({
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
},
|
|
70
|
+
entryPoints: [
|
|
71
|
+
'node_modules/single-file-core/single-file-archive.js'
|
|
72
|
+
],
|
|
74
73
|
bundle: true,
|
|
75
74
|
outfile: 'lib/single-file-archive.js',
|
|
76
75
|
platform: 'neutral',
|
package/deno.json
CHANGED
package/dev-build.sh
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
cp -R ../single-file-core/* node_modules/single-file-core
|
|
3
|
+
set -e
|
|
5
4
|
|
|
6
5
|
echo "
|
|
7
6
|
import { build } from 'npm:esbuild';
|
|
8
7
|
|
|
9
8
|
await build({
|
|
10
9
|
entryPoints: [
|
|
11
|
-
'
|
|
10
|
+
'../single-file-core/single-file.js'
|
|
12
11
|
],
|
|
13
12
|
bundle: true,
|
|
14
13
|
globalName: 'singlefile',
|
|
@@ -22,7 +21,7 @@ await build({
|
|
|
22
21
|
|
|
23
22
|
await build({
|
|
24
23
|
entryPoints: [
|
|
25
|
-
'
|
|
24
|
+
'../single-file-core/single-file-bootstrap.js'
|
|
26
25
|
],
|
|
27
26
|
bundle: true,
|
|
28
27
|
globalName: 'singlefileBootstrap',
|
|
@@ -36,7 +35,7 @@ await build({
|
|
|
36
35
|
|
|
37
36
|
await build({
|
|
38
37
|
entryPoints: [
|
|
39
|
-
'
|
|
38
|
+
'../single-file-core/single-file-hooks-frames.js'
|
|
40
39
|
],
|
|
41
40
|
bundle: true,
|
|
42
41
|
outdir: 'lib/',
|
|
@@ -49,7 +48,7 @@ await build({
|
|
|
49
48
|
|
|
50
49
|
await build({
|
|
51
50
|
entryPoints: [
|
|
52
|
-
'
|
|
51
|
+
'../single-file-core/vendor/zip/zip.min.js'
|
|
53
52
|
],
|
|
54
53
|
bundle: true,
|
|
55
54
|
globalName: 'zip',
|
|
@@ -62,10 +61,9 @@ await build({
|
|
|
62
61
|
});
|
|
63
62
|
|
|
64
63
|
await build({
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
},
|
|
64
|
+
entryPoints: [
|
|
65
|
+
'../single-file-core/single-file-archive.js'
|
|
66
|
+
],
|
|
69
67
|
bundle: true,
|
|
70
68
|
outfile: 'lib/single-file-archive.js',
|
|
71
69
|
platform: 'neutral',
|
|
@@ -95,6 +93,4 @@ await Promise.all(SCRIPTS.map(script => Deno.remove(script)));
|
|
|
95
93
|
await Deno.remove('lib/single-file-hooks-frames.js');
|
|
96
94
|
const version = JSON.parse(await Deno.readTextFile('./deno.json')).version;
|
|
97
95
|
await Deno.writeTextFile('lib/version.js', 'export const version = ' + JSON.stringify(version) + ';');
|
|
98
|
-
" | deno run --allow-read --allow-write --allow-net --allow-run --allow-env --lock=node_modules/deno.lock.tmp -
|
|
99
|
-
|
|
100
|
-
rm -rf node_modules
|
|
96
|
+
" | deno run --allow-read --allow-write --allow-net --allow-run --allow-env --lock=node_modules/deno.lock.tmp -
|
package/lib/browser.js
CHANGED
|
@@ -21,27 +21,90 @@
|
|
|
21
21
|
* Source.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
/* global fetch, setTimeout */
|
|
24
|
+
/* global fetch, setTimeout, WebSocket */
|
|
25
25
|
|
|
26
26
|
import { BROWSER_PATHS, BROWSER_ARGS } from "./constants.js";
|
|
27
|
-
import { Deno } from "./deno-polyfill.js";
|
|
27
|
+
import { Deno, path } from "./deno-polyfill.js";
|
|
28
28
|
|
|
29
29
|
const NULL_STD_CONFIG = "null";
|
|
30
30
|
const DEBUG_PORT_MIN = 9222;
|
|
31
31
|
const DEBUG_PORT_RANGE = 256;
|
|
32
32
|
const READY_TIMEOUT = 30000;
|
|
33
33
|
const READY_RETRY_DELAY = 250;
|
|
34
|
+
const CLOSE_TIMEOUT = 15000;
|
|
35
|
+
const LOCALHOST = "http://localhost:";
|
|
36
|
+
const PROFILE_LOCK_ENTRY_PREFIX = "Singleton";
|
|
37
|
+
const PROFILE_IGNORED_ENTRY_NAMES = [
|
|
38
|
+
"AutofillAiModelCache",
|
|
39
|
+
"Cache",
|
|
40
|
+
"Code Cache",
|
|
41
|
+
"CacheStorage",
|
|
42
|
+
"Crashpad",
|
|
43
|
+
"DawnCache",
|
|
44
|
+
"DawnGraphiteCache",
|
|
45
|
+
"DawnWebGPUCache",
|
|
46
|
+
"GPUCache",
|
|
47
|
+
"GPUPersistentCache",
|
|
48
|
+
"GraphiteDawnCache",
|
|
49
|
+
"GrShaderCache",
|
|
50
|
+
"ShaderCache",
|
|
51
|
+
"component_crx_cache",
|
|
52
|
+
"extensions_crx_cache",
|
|
53
|
+
"optimization_guide_hint_cache_store",
|
|
54
|
+
"optimization_guide_model_store"
|
|
55
|
+
];
|
|
56
|
+
const PROFILE_INCOMPATIBLE_ARGS = [
|
|
57
|
+
"--no-startup-window",
|
|
58
|
+
"--bwsi",
|
|
59
|
+
"--deny-permission-prompts"
|
|
60
|
+
];
|
|
34
61
|
|
|
35
|
-
const { build, makeTempDir, Command, errors, remove } = Deno;
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
const { build, makeTempDir, readDir, copyFile, mkdir, Command, errors, remove } = Deno;
|
|
63
|
+
const { join } = path;
|
|
64
|
+
let child, profilePath, childExited, keepProfilePath, browserDebugPort;
|
|
65
|
+
export { launchBrowser, createBrowserProfile, getBrowserOptions, copyProfile, closeBrowser, browserExited };
|
|
66
|
+
|
|
67
|
+
function getBrowserOptions(options) {
|
|
68
|
+
return {
|
|
69
|
+
args: options.browserArgs,
|
|
70
|
+
headless: options.browserHeadless,
|
|
71
|
+
executablePath: options.browserExecutablePath,
|
|
72
|
+
debug: options.browserDebug,
|
|
73
|
+
singleProcess: options.browserSingleProcess,
|
|
74
|
+
disableWebSecurity: options.browserDisableWebSecurity,
|
|
75
|
+
width: options.browserWidth,
|
|
76
|
+
height: options.browserHeight,
|
|
77
|
+
userAgent: options.userAgent,
|
|
78
|
+
httpProxyServer: options.httpProxyServer,
|
|
79
|
+
profile: options.browserProfile
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function createBrowserProfile(options = {}) {
|
|
84
|
+
await mkdir(options.profile, { recursive: true });
|
|
85
|
+
await launchBrowser(Object.assign({}, options, { headless: false, persistProfile: true }));
|
|
86
|
+
if (child !== undefined) {
|
|
87
|
+
await child.status;
|
|
88
|
+
}
|
|
89
|
+
await closeBrowser();
|
|
90
|
+
}
|
|
38
91
|
|
|
39
92
|
async function launchBrowser(options = {}, indexPath = 0) {
|
|
40
93
|
const executablePath = options.executablePath || BROWSER_PATHS[build.os][indexPath];
|
|
41
94
|
let args = Array.from(BROWSER_ARGS);
|
|
42
95
|
const debugPort = await getDebugPort();
|
|
96
|
+
browserDebugPort = debugPort;
|
|
43
97
|
args.push("--remote-debugging-port=" + debugPort);
|
|
44
|
-
|
|
98
|
+
keepProfilePath = Boolean(options.persistProfile);
|
|
99
|
+
if (keepProfilePath) {
|
|
100
|
+
profilePath = options.profile;
|
|
101
|
+
args = args.filter(arg => !PROFILE_INCOMPATIBLE_ARGS.includes(arg));
|
|
102
|
+
} else {
|
|
103
|
+
profilePath = await makeTempDir();
|
|
104
|
+
if (options.profile) {
|
|
105
|
+
await copyProfile(options.profile, profilePath);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
45
108
|
if (options.headless && !options.debug) {
|
|
46
109
|
args.push("--headless");
|
|
47
110
|
} else {
|
|
@@ -74,11 +137,16 @@ async function launchBrowser(options = {}, indexPath = 0) {
|
|
|
74
137
|
!args.includes("--headless")) {
|
|
75
138
|
args.push("--disable-site-isolation-trials");
|
|
76
139
|
}
|
|
140
|
+
if (options.startUrl) {
|
|
141
|
+
args.push(options.startUrl);
|
|
142
|
+
}
|
|
77
143
|
const command = new Command(executablePath, { args, stdout: NULL_STD_CONFIG, stderr: NULL_STD_CONFIG });
|
|
78
144
|
try {
|
|
79
145
|
child = await command.spawn();
|
|
80
146
|
} catch (error) {
|
|
81
|
-
|
|
147
|
+
if (!keepProfilePath) {
|
|
148
|
+
await remove(profilePath, { recursive: true }).catch(() => { });
|
|
149
|
+
}
|
|
82
150
|
profilePath = undefined;
|
|
83
151
|
if (error instanceof errors.NotFound) {
|
|
84
152
|
if (!options.executablePath && indexPath + 1 < BROWSER_PATHS[build.os].length) {
|
|
@@ -155,21 +223,86 @@ function getRandomDebugPort() {
|
|
|
155
223
|
}
|
|
156
224
|
|
|
157
225
|
async function closeBrowser() {
|
|
158
|
-
|
|
226
|
+
const closedChild = child;
|
|
227
|
+
const closedProfilePath = profilePath;
|
|
228
|
+
child = undefined;
|
|
229
|
+
profilePath = undefined;
|
|
230
|
+
if (closedChild !== undefined) {
|
|
231
|
+
if (keepProfilePath && !childExited) {
|
|
232
|
+
await closeBrowserGracefully(closedChild);
|
|
233
|
+
}
|
|
159
234
|
try {
|
|
160
|
-
|
|
235
|
+
closedChild.kill();
|
|
161
236
|
} catch {
|
|
162
237
|
// ignored
|
|
163
238
|
}
|
|
164
|
-
await
|
|
165
|
-
child = undefined;
|
|
239
|
+
await closedChild.status;
|
|
166
240
|
}
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
await
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
241
|
+
if (closedProfilePath !== undefined) {
|
|
242
|
+
if (keepProfilePath) {
|
|
243
|
+
await removeProfileLockEntries(closedProfilePath).catch(() => { });
|
|
244
|
+
} else {
|
|
245
|
+
try {
|
|
246
|
+
await remove(closedProfilePath, { recursive: true });
|
|
247
|
+
} catch {
|
|
248
|
+
console.log("Warning: failed to remove profile directory: " + closedProfilePath); // eslint-disable-line no-console
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function closeBrowserGracefully(closedChild) {
|
|
255
|
+
try {
|
|
256
|
+
const response = await fetch(LOCALHOST + browserDebugPort + "/json/version");
|
|
257
|
+
const { webSocketDebuggerUrl } = await response.json();
|
|
258
|
+
const socket = new WebSocket(webSocketDebuggerUrl);
|
|
259
|
+
await new Promise((resolve, reject) => {
|
|
260
|
+
socket.addEventListener("open", resolve);
|
|
261
|
+
socket.addEventListener("error", reject);
|
|
262
|
+
});
|
|
263
|
+
socket.send(JSON.stringify({ id: 0, method: "Browser.close" }));
|
|
264
|
+
await Promise.race([
|
|
265
|
+
closedChild.status,
|
|
266
|
+
new Promise(resolve => setTimeout(resolve, CLOSE_TIMEOUT))
|
|
267
|
+
]);
|
|
268
|
+
} catch {
|
|
269
|
+
// ignored
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function copyProfile(sourcePath, destinationPath) {
|
|
274
|
+
let entries;
|
|
275
|
+
try {
|
|
276
|
+
entries = await readDir(sourcePath);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
throw error instanceof errors.NotFound ?
|
|
279
|
+
new Error(`The browser profile directory was not found at ${JSON.stringify(sourcePath)}`) :
|
|
280
|
+
error;
|
|
281
|
+
}
|
|
282
|
+
await copyProfileEntries(entries, sourcePath, destinationPath);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function copyProfileEntries(entries, sourcePath, destinationPath) {
|
|
286
|
+
for (const entry of entries) {
|
|
287
|
+
if (entry.isSymlink || entry.name.startsWith(PROFILE_LOCK_ENTRY_PREFIX) || PROFILE_IGNORED_ENTRY_NAMES.includes(entry.name)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const entrySourcePath = join(sourcePath, entry.name);
|
|
291
|
+
const entryDestinationPath = join(destinationPath, entry.name);
|
|
292
|
+
if (entry.isDirectory) {
|
|
293
|
+
await mkdir(entryDestinationPath, { recursive: true });
|
|
294
|
+
await copyProfileEntries(await readDir(entrySourcePath), entrySourcePath, entryDestinationPath);
|
|
295
|
+
} else {
|
|
296
|
+
await copyFile(entrySourcePath, entryDestinationPath);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function removeProfileLockEntries(profileDirectory) {
|
|
302
|
+
const entries = await readDir(profileDirectory);
|
|
303
|
+
for (const entry of entries) {
|
|
304
|
+
if (entry.name.startsWith(PROFILE_LOCK_ENTRY_PREFIX)) {
|
|
305
|
+
await remove(join(profileDirectory, entry.name)).catch(() => { });
|
|
173
306
|
}
|
|
174
307
|
}
|
|
175
308
|
}
|