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 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.95"
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
- stdin: {
71
- contents: \"export * from './processors/compression/compression.js'; export * from './vendor/zip/zip.js';\",
72
- resolveDir: 'node_modules/single-file-core',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@single-file/single-file-cli",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "SingleFile CLI",
5
5
  "exports": {
6
6
  ".": "./single-file-cli-api.js"
package/dev-build.sh CHANGED
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
- mkdir -p node_modules/single-file-core
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
- 'node_modules/single-file-core/single-file.js'
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
- 'node_modules/single-file-core/single-file-bootstrap.js'
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
- 'node_modules/single-file-core/single-file-hooks-frames.js'
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
- 'node_modules/single-file-core/vendor/zip/zip.min.js'
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
- stdin: {
66
- contents: \"export * from './processors/compression/compression.js'; export * from './vendor/zip/zip.js';\",
67
- resolveDir: 'node_modules/single-file-core',
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
- let child, profilePath, childExited;
37
- export { launchBrowser, closeBrowser, browserExited };
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
- profilePath = await makeTempDir();
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
- await remove(profilePath, { recursive: true }).catch(() => { });
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
- if (child !== undefined) {
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
- child.kill();
235
+ closedChild.kill();
161
236
  } catch {
162
237
  // ignored
163
238
  }
164
- await child.status;
165
- child = undefined;
239
+ await closedChild.status;
166
240
  }
167
- if (profilePath !== undefined) {
168
- try {
169
- await remove(profilePath, { recursive: true });
170
- profilePath = undefined;
171
- } catch {
172
- console.log("Warning: failed to remove profile directory: " + profilePath); // eslint-disable-line no-console
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
  }