@jbrowse/capture 5.0.0-beta.13 → 5.0.0-beta.14
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 +31 -0
- package/esm/annotationOverlay.js +1 -1
- package/esm/args.d.ts +3 -2
- package/esm/args.js +12 -9
- package/esm/bin.js +86 -2
- package/esm/capture.d.ts +22 -0
- package/esm/capture.js +130 -38
- package/esm/index.d.ts +3 -2
- package/esm/index.js +1 -1
- package/esm/jsonArgs.d.ts +19 -0
- package/esm/jsonArgs.js +13 -1
- package/esm/poll.d.ts +4 -2
- package/esm/poll.js +7 -4
- package/esm/ready.d.ts +3 -3
- package/esm/sessionGate.d.ts +2 -2
- package/esm/url.d.ts +1 -1
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/esm/waits.d.ts +5 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -208,6 +208,37 @@ picture with the callout missing or parked in a corner.
|
|
|
208
208
|
In a script, pass the same array as `annotations` to `captureJBrowse`, or call
|
|
209
209
|
`drawAnnotations(page, annotations)` on the page `openJBrowse` returns.
|
|
210
210
|
|
|
211
|
+
## Many screenshots from one browser
|
|
212
|
+
|
|
213
|
+
`jb2capture batch` takes a JSON array with one object per image and draws them
|
|
214
|
+
all from one browser. Each image gets a window of its own, closed after its
|
|
215
|
+
screenshot, so no session state passes from one image to the next.
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
jb2capture batch captures.json --hub hg38 --track hg38-ncbiRefSeqCurated
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
[
|
|
223
|
+
{ "loc": "BRCA1", "out": "brca1.png" },
|
|
224
|
+
{ "loc": "TP53", "out": "tp53.png", "tracks": ["hg38-clinvarMain"] }
|
|
225
|
+
]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
An object names its `out` and may set `hub`, `config`, `assembly`, `loc`,
|
|
229
|
+
`tracks`, `spec`, `session`, `width`, `height`, `dpr`, `fullPage` and
|
|
230
|
+
`annotations`. The command-line flags are the defaults for what an object leaves
|
|
231
|
+
out. `--concurrency` sets how many windows are open at once (default 4).
|
|
232
|
+
|
|
233
|
+
Each image is reported on a line of its own with the time it took. A failed
|
|
234
|
+
image is reported and the rest carry on, a browser that died is relaunched for
|
|
235
|
+
the images left, and the exit code is 1 if any image failed. Chrome flags and
|
|
236
|
+
`--headed` apply to the whole batch, so images that need different ones go in
|
|
237
|
+
separate batches.
|
|
238
|
+
|
|
239
|
+
In a script, `captureBatch(captures, { concurrency })` takes an array of
|
|
240
|
+
`captureJBrowse` options and returns one result per capture, in order.
|
|
241
|
+
|
|
211
242
|
## CLI
|
|
212
243
|
|
|
213
244
|
`jb2capture --help` for the full list. Also:
|
package/esm/annotationOverlay.js
CHANGED
|
@@ -534,7 +534,7 @@ export function drawAnnotationOverlay(items, overlayId) {
|
|
|
534
534
|
rect.setAttribute('height', String(a.height ?? 0));
|
|
535
535
|
rect.setAttribute('rx', '4');
|
|
536
536
|
// a positive fillOpacity tints the box with a translucent wash of its
|
|
537
|
-
// own
|
|
537
|
+
// own color (a "light green/orange" highlight); otherwise hollow
|
|
538
538
|
rect.setAttribute('fill', a.fillOpacity ? color : 'none');
|
|
539
539
|
if (a.fillOpacity) {
|
|
540
540
|
rect.setAttribute('fill-opacity', String(a.fillOpacity));
|
package/esm/args.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface ParsedArgs {
|
|
2
|
-
command: 'capture' | 'url' | 'list';
|
|
2
|
+
command: 'capture' | 'batch' | 'url' | 'list';
|
|
3
3
|
hub?: string;
|
|
4
4
|
config?: string;
|
|
5
5
|
assembly?: string;
|
|
@@ -13,6 +13,7 @@ export interface ParsedArgs {
|
|
|
13
13
|
height?: number;
|
|
14
14
|
dpr?: number;
|
|
15
15
|
timeout?: number;
|
|
16
|
+
concurrency?: number;
|
|
16
17
|
fullPage: boolean;
|
|
17
18
|
annotations?: string;
|
|
18
19
|
headed: boolean;
|
|
@@ -20,7 +21,7 @@ export interface ParsedArgs {
|
|
|
20
21
|
help: boolean;
|
|
21
22
|
version: boolean;
|
|
22
23
|
allowUnsettled: boolean;
|
|
23
|
-
/** `list`'s hub and filter. Empty for the other commands. */
|
|
24
|
+
/** `list`'s hub and filter, or `batch`'s manifest. Empty for the other commands. */
|
|
24
25
|
positionals: string[];
|
|
25
26
|
}
|
|
26
27
|
/**
|
package/esm/args.js
CHANGED
|
@@ -15,6 +15,7 @@ const OPTIONS = {
|
|
|
15
15
|
height: { type: 'string' },
|
|
16
16
|
dpr: { type: 'string' },
|
|
17
17
|
timeout: { type: 'string' },
|
|
18
|
+
concurrency: { type: 'string' },
|
|
18
19
|
fullPage: { type: 'boolean', default: false },
|
|
19
20
|
annotations: { type: 'string' },
|
|
20
21
|
headed: { type: 'boolean', default: false },
|
|
@@ -25,6 +26,8 @@ const OPTIONS = {
|
|
|
25
26
|
};
|
|
26
27
|
const EVERY_COMMAND = ['help', 'version'];
|
|
27
28
|
const SUBCOMMAND_FLAGS = {
|
|
29
|
+
capture: new Set(Object.keys(OPTIONS).filter(f => f !== 'concurrency')),
|
|
30
|
+
batch: new Set(Object.keys(OPTIONS).filter(f => f !== 'out')),
|
|
28
31
|
url: new Set([
|
|
29
32
|
'hub',
|
|
30
33
|
'config',
|
|
@@ -69,26 +72,25 @@ function positive(name, raw) {
|
|
|
69
72
|
*/
|
|
70
73
|
export function parseArgs(argv) {
|
|
71
74
|
const [first, ...afterCommand] = argv;
|
|
72
|
-
const command = first === 'list' || first === 'url' ? first : 'capture';
|
|
75
|
+
const command = first === 'list' || first === 'url' || first === 'batch' ? first : 'capture';
|
|
73
76
|
const { values, positionals, tokens } = parseNodeArgs({
|
|
74
77
|
args: command === 'capture' ? argv : afterCommand,
|
|
75
78
|
options: OPTIONS,
|
|
76
|
-
allowPositionals: command === 'list',
|
|
79
|
+
allowPositionals: command === 'list' || command === 'batch',
|
|
77
80
|
tokens: true,
|
|
78
81
|
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
82
|
+
for (const token of tokens) {
|
|
83
|
+
if (token.kind === 'option' && !SUBCOMMAND_FLAGS[command].has(token.name)) {
|
|
84
|
+
throw new Error(command === 'capture'
|
|
85
|
+
? `${token.rawName} applies only to \`jb2capture batch\``
|
|
86
|
+
: `${token.rawName} does not apply to \`jb2capture ${command}\``);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
const fromStdin = ['spec', 'session', 'annotations'].filter(flag => values[flag] === '-');
|
|
88
90
|
if (fromStdin.length > 1) {
|
|
89
91
|
throw new Error(`${fromStdin.map(f => `--${f}`).join(' and ')} cannot both read stdin (-)`);
|
|
90
92
|
}
|
|
91
|
-
const { track, width, height, dpr, timeout, ...rest } = values;
|
|
93
|
+
const { track, width, height, dpr, timeout, concurrency, ...rest } = values;
|
|
92
94
|
return {
|
|
93
95
|
...rest,
|
|
94
96
|
command,
|
|
@@ -98,6 +100,7 @@ export function parseArgs(argv) {
|
|
|
98
100
|
height: positiveInt('height', height),
|
|
99
101
|
dpr: positive('dpr', dpr),
|
|
100
102
|
timeout: positive('timeout', timeout),
|
|
103
|
+
concurrency: positiveInt('concurrency', concurrency),
|
|
101
104
|
positionals,
|
|
102
105
|
};
|
|
103
106
|
}
|
package/esm/bin.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from './args.js';
|
|
3
|
-
import { captureJBrowse } from './capture.js';
|
|
3
|
+
import { captureBatch, captureJBrowse } from './capture.js';
|
|
4
4
|
import { resolveAgainstConfig } from './catalog.js';
|
|
5
5
|
import { listHubAssemblies, listHubTracks, trackName } from './hub.js';
|
|
6
|
-
import { readAnnotations, readJson, readSpec } from './jsonArgs.js';
|
|
6
|
+
import { readAnnotations, readBatch, readJson, readSpec } from './jsonArgs.js';
|
|
7
7
|
import { PUBLIC_INSTANCE, jbrowseUrl } from './url.js';
|
|
8
8
|
import { version } from './version.js';
|
|
9
9
|
const HELP = `jb2capture — screenshot a live JBrowse 2 view, once it has finished drawing
|
|
10
10
|
|
|
11
11
|
USAGE
|
|
12
12
|
jb2capture [flags] --out <file.png> open a view, wait for it, screenshot it
|
|
13
|
+
jb2capture batch <json|path|-> [flags] screenshot many views from one browser
|
|
13
14
|
jb2capture url [flags] print the URL instead of launching a browser
|
|
14
15
|
jb2capture list [hub] [filter] list hosted assemblies, or one's trackIds
|
|
15
16
|
|
|
@@ -46,6 +47,16 @@ WAITING
|
|
|
46
47
|
|
|
47
48
|
Either fails the run rather than writing a frame that is not the picture.
|
|
48
49
|
|
|
50
|
+
BATCH
|
|
51
|
+
The manifest is a JSON array with one object per image. Each names its "out"
|
|
52
|
+
and may set hub, config, assembly, loc, tracks, spec, session, width, height,
|
|
53
|
+
dpr, fullPage and annotations; the flags above are the defaults for what an
|
|
54
|
+
object leaves out. Every image gets a page of its own in one shared browser.
|
|
55
|
+
A failed image is reported and the rest carry on; the exit code is 1 if any
|
|
56
|
+
failed.
|
|
57
|
+
|
|
58
|
+
--concurrency <n> pages open at once (default 4)
|
|
59
|
+
|
|
49
60
|
OTHER
|
|
50
61
|
--headed run with a visible browser window
|
|
51
62
|
--verbose print browser console output and uncaught errors
|
|
@@ -61,6 +72,10 @@ EXAMPLES
|
|
|
61
72
|
## find the trackIds first
|
|
62
73
|
jb2capture list hg38 conservation
|
|
63
74
|
|
|
75
|
+
## one image per locus, from one browser
|
|
76
|
+
jb2capture batch '[{"loc":"BRCA1","out":"brca1.png"},{"loc":"TP53","out":"tp53.png"}]' \\
|
|
77
|
+
--hub hg38 --track hg38-ncbiRefSeqCurated
|
|
78
|
+
|
|
64
79
|
## your own config, two loci side by side
|
|
65
80
|
jb2capture --config https://example.org/config.json --assembly mydata \\
|
|
66
81
|
--loc "chr3:25,325,000-25,361,000 chr10:58,716,500-58,718,500" -o two.png
|
|
@@ -77,6 +92,71 @@ function urlOptions(args) {
|
|
|
77
92
|
instance: args.instance,
|
|
78
93
|
};
|
|
79
94
|
}
|
|
95
|
+
async function runBatch(args) {
|
|
96
|
+
const [manifest, ...extra] = args.positionals;
|
|
97
|
+
if (!manifest || extra.length) {
|
|
98
|
+
throw new Error('`jb2capture batch` takes one manifest: JSON, a path or -');
|
|
99
|
+
}
|
|
100
|
+
const defaults = {
|
|
101
|
+
...urlOptions(args),
|
|
102
|
+
width: args.width,
|
|
103
|
+
height: args.height,
|
|
104
|
+
dpr: args.dpr,
|
|
105
|
+
fullPage: args.fullPage,
|
|
106
|
+
annotations: args.annotations
|
|
107
|
+
? readAnnotations(args.annotations)
|
|
108
|
+
: undefined,
|
|
109
|
+
timeout: args.timeout,
|
|
110
|
+
allowUnsettled: args.allowUnsettled,
|
|
111
|
+
onConsole: args.verbose
|
|
112
|
+
? (text) => {
|
|
113
|
+
console.error(` [page] ${text}`);
|
|
114
|
+
}
|
|
115
|
+
: undefined,
|
|
116
|
+
};
|
|
117
|
+
const captures = readBatch(manifest).map(({ spec, session, annotations, ...entry }) => ({
|
|
118
|
+
...defaults,
|
|
119
|
+
// an entry that says what to open replaces the default session whole
|
|
120
|
+
...(spec || session ? { spec: undefined, session: undefined } : {}),
|
|
121
|
+
...entry,
|
|
122
|
+
...(spec
|
|
123
|
+
? { spec: typeof spec === 'string' ? readSpec(spec) : spec }
|
|
124
|
+
: {}),
|
|
125
|
+
...(session
|
|
126
|
+
? {
|
|
127
|
+
session: typeof session === 'string'
|
|
128
|
+
? readJson('session', session)
|
|
129
|
+
: session,
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
132
|
+
...(annotations
|
|
133
|
+
? {
|
|
134
|
+
annotations: typeof annotations === 'string'
|
|
135
|
+
? readAnnotations(annotations)
|
|
136
|
+
: annotations,
|
|
137
|
+
}
|
|
138
|
+
: {}),
|
|
139
|
+
}));
|
|
140
|
+
const start = performance.now();
|
|
141
|
+
const results = await captureBatch(captures, {
|
|
142
|
+
headless: !args.headed,
|
|
143
|
+
concurrency: args.concurrency,
|
|
144
|
+
onResult: (result, index) => {
|
|
145
|
+
const label = `[${index + 1}/${captures.length}] ${result.out} ${(result.ms / 1000).toFixed(1)}s`;
|
|
146
|
+
if (result.ok) {
|
|
147
|
+
console.log(`wrote ${label}`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
console.error(`FAILED ${label}: ${result.error.message}`);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const failed = results.filter(r => !r.ok).length;
|
|
155
|
+
console.log(`wrote ${results.length - failed}/${results.length} images in ${((performance.now() - start) / 1000).toFixed(1)}s`);
|
|
156
|
+
if (failed) {
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
80
160
|
async function runList(positionals) {
|
|
81
161
|
const [hub, filter] = positionals;
|
|
82
162
|
if (!hub) {
|
|
@@ -117,6 +197,10 @@ async function main() {
|
|
|
117
197
|
await runList(args.positionals);
|
|
118
198
|
return;
|
|
119
199
|
}
|
|
200
|
+
if (args.command === 'batch') {
|
|
201
|
+
await runBatch(args);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
120
204
|
if (args.command === 'url') {
|
|
121
205
|
console.log(jbrowseUrl(await resolveAgainstConfig(urlOptions(args))));
|
|
122
206
|
return;
|
package/esm/capture.d.ts
CHANGED
|
@@ -50,3 +50,25 @@ export interface CaptureResult extends ReadyReport {
|
|
|
50
50
|
* browser.
|
|
51
51
|
*/
|
|
52
52
|
export declare function captureJBrowse(options?: CaptureOptions): Promise<CaptureResult>;
|
|
53
|
+
export interface BatchOptions extends LaunchOptions {
|
|
54
|
+
/** Pages open at once. Default 4. */
|
|
55
|
+
concurrency?: number;
|
|
56
|
+
/** Called as each capture finishes, in completion order. */
|
|
57
|
+
onResult?: (result: BatchResult, index: number) => void;
|
|
58
|
+
}
|
|
59
|
+
export type BatchResult = {
|
|
60
|
+
out?: string;
|
|
61
|
+
ms: number;
|
|
62
|
+
} & (({
|
|
63
|
+
ok: true;
|
|
64
|
+
} & CaptureResult) | {
|
|
65
|
+
ok: false;
|
|
66
|
+
error: Error;
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* Screenshot many sessions from one browser, each on a page of its own that is
|
|
70
|
+
* closed after its screenshot. A failed capture is reported in its result and
|
|
71
|
+
* the rest carry on; a browser that died is relaunched for the captures left.
|
|
72
|
+
* Results come back in the order of `captures`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function captureBatch(captures: CaptureOptions[], { concurrency, onResult, ...launch }?: BatchOptions): Promise<BatchResult[]>;
|
package/esm/capture.js
CHANGED
|
@@ -11,13 +11,7 @@ import { waitForFrame, waitForJBrowseReady } from './ready.js';
|
|
|
11
11
|
import { assemblyFromSession, savedSnapshot, trackIdsFromSession, } from './session.js';
|
|
12
12
|
import { sessionOverflowInPage } from './sessionOverflow.js';
|
|
13
13
|
import { PUBLIC_INSTANCE, assertCoherentOptions, jbrowseUrl } from './url.js';
|
|
14
|
-
|
|
15
|
-
* Launch a browser, open a JBrowse session, and wait until it has rendered.
|
|
16
|
-
* The caller owns the returned browser and must close it. The assembly and
|
|
17
|
-
* tracks asked for become the session gate's expectations unless `trackIds`
|
|
18
|
-
* names others.
|
|
19
|
-
*/
|
|
20
|
-
export async function openJBrowse(options = {}) {
|
|
14
|
+
async function prepareOpen(options) {
|
|
21
15
|
const { width = 1400, height = 900, dpr = 2, headless, executablePath, args, onConsole, timeout = DEFAULT_TIMEOUT, trackIds, ...given } = options;
|
|
22
16
|
assertCoherentOptions(given);
|
|
23
17
|
const [urlOptions] = await Promise.all([
|
|
@@ -36,10 +30,24 @@ export async function openJBrowse(options = {}) {
|
|
|
36
30
|
? sessionAssembly &&
|
|
37
31
|
(await canonicalSessionAssembly(urlOptions, sessionAssembly))
|
|
38
32
|
: (assembly ?? hub);
|
|
39
|
-
|
|
33
|
+
return {
|
|
34
|
+
url,
|
|
35
|
+
viewport: { width, height, deviceScaleFactor: dpr },
|
|
36
|
+
onConsole,
|
|
37
|
+
timeout,
|
|
38
|
+
ready: {
|
|
39
|
+
...options,
|
|
40
|
+
assembly: expectedAssembly,
|
|
41
|
+
trackIds: trackIds ?? (opens ? trackIdsFromSession(opens) : tracks),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function openPage(browser, { url, viewport, onConsole, timeout, ready, }) {
|
|
46
|
+
// a window each: a tab behind another stops painting, and a batch has
|
|
47
|
+
// several pages open
|
|
48
|
+
const page = await browser.newPage({ type: 'window' });
|
|
40
49
|
try {
|
|
41
|
-
|
|
42
|
-
await page.setViewport({ width, height, deviceScaleFactor: dpr });
|
|
50
|
+
await page.setViewport(viewport);
|
|
43
51
|
if (onConsole) {
|
|
44
52
|
page.on('console', msg => {
|
|
45
53
|
const text = msg.text();
|
|
@@ -68,27 +76,36 @@ export async function openJBrowse(options = {}) {
|
|
|
68
76
|
waitUntil: 'domcontentloaded',
|
|
69
77
|
timeout,
|
|
70
78
|
});
|
|
71
|
-
|
|
79
|
+
// 304: a page after the first in a batch comes from the shared cache
|
|
80
|
+
if (response && !response.ok() && response.status() !== 304) {
|
|
72
81
|
throw new Error(`${url} answered HTTP ${response.status()}. Check --instance.`);
|
|
73
82
|
}
|
|
74
|
-
const report = await waitForJBrowseReady(page,
|
|
75
|
-
|
|
76
|
-
assembly: expectedAssembly,
|
|
77
|
-
trackIds: trackIds ?? (opens ? trackIdsFromSession(opens) : tracks),
|
|
78
|
-
});
|
|
79
|
-
return { browser, page, url, ...report };
|
|
83
|
+
const report = await waitForJBrowseReady(page, ready);
|
|
84
|
+
return { page, url, ...report };
|
|
80
85
|
}
|
|
81
86
|
catch (error) {
|
|
82
|
-
await
|
|
87
|
+
await page.close().catch(() => { });
|
|
83
88
|
throw error;
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
/**
|
|
87
|
-
*
|
|
88
|
-
* browser.
|
|
92
|
+
* Launch a browser, open a JBrowse session, and wait until it has rendered.
|
|
93
|
+
* The caller owns the returned browser and must close it. The assembly and
|
|
94
|
+
* tracks asked for become the session gate's expectations unless `trackIds`
|
|
95
|
+
* names others.
|
|
89
96
|
*/
|
|
90
|
-
export async function
|
|
91
|
-
const
|
|
97
|
+
export async function openJBrowse(options = {}) {
|
|
98
|
+
const prepared = await prepareOpen(options);
|
|
99
|
+
const browser = await launchBrowser(options);
|
|
100
|
+
try {
|
|
101
|
+
return { browser, ...(await openPage(browser, prepared)) };
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
await browser.close().catch(() => { });
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function assertCapturable({ out, annotations }) {
|
|
92
109
|
if (annotations) {
|
|
93
110
|
assertValidAnnotations(annotations);
|
|
94
111
|
}
|
|
@@ -96,25 +113,100 @@ export async function captureJBrowse(options = {}) {
|
|
|
96
113
|
assertImagePath(out);
|
|
97
114
|
mkdirSync(dirname(out), { recursive: true });
|
|
98
115
|
}
|
|
99
|
-
|
|
116
|
+
}
|
|
117
|
+
async function shoot(page, opened, { out, fullPage = false, annotations, ...openOptions }) {
|
|
118
|
+
let report = opened;
|
|
119
|
+
const overflow = fullPage ? await page.evaluate(sessionOverflowInPage) : 0;
|
|
120
|
+
const viewport = page.viewport();
|
|
121
|
+
if (viewport && overflow > 0) {
|
|
122
|
+
await page.setViewport({
|
|
123
|
+
...viewport,
|
|
124
|
+
height: viewport.height + overflow,
|
|
125
|
+
});
|
|
126
|
+
report = await waitForFrame(page, openOptions);
|
|
127
|
+
}
|
|
128
|
+
if (annotations?.length) {
|
|
129
|
+
await drawAnnotations(page, annotations);
|
|
130
|
+
}
|
|
131
|
+
const image = await page.screenshot({ path: out });
|
|
132
|
+
return { image, ...report };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Open a JBrowse session, wait for it to render, screenshot it, and close the
|
|
136
|
+
* browser.
|
|
137
|
+
*/
|
|
138
|
+
export async function captureJBrowse(options = {}) {
|
|
139
|
+
assertCapturable(options);
|
|
140
|
+
const { browser, page, url, ...opened } = await openJBrowse(options);
|
|
100
141
|
try {
|
|
101
|
-
|
|
102
|
-
const overflow = fullPage ? await page.evaluate(sessionOverflowInPage) : 0;
|
|
103
|
-
const viewport = page.viewport();
|
|
104
|
-
if (viewport && overflow > 0) {
|
|
105
|
-
await page.setViewport({
|
|
106
|
-
...viewport,
|
|
107
|
-
height: viewport.height + overflow,
|
|
108
|
-
});
|
|
109
|
-
report = await waitForFrame(page, openOptions);
|
|
110
|
-
}
|
|
111
|
-
if (annotations?.length) {
|
|
112
|
-
await drawAnnotations(page, annotations);
|
|
113
|
-
}
|
|
114
|
-
const image = await page.screenshot({ path: out });
|
|
115
|
-
return { url, image, ...report };
|
|
142
|
+
return { url, ...(await shoot(page, opened, options)) };
|
|
116
143
|
}
|
|
117
144
|
finally {
|
|
118
145
|
await browser.close().catch(() => { });
|
|
119
146
|
}
|
|
120
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Screenshot many sessions from one browser, each on a page of its own that is
|
|
150
|
+
* closed after its screenshot. A failed capture is reported in its result and
|
|
151
|
+
* the rest carry on; a browser that died is relaunched for the captures left.
|
|
152
|
+
* Results come back in the order of `captures`.
|
|
153
|
+
*/
|
|
154
|
+
export async function captureBatch(captures, { concurrency = 4, onResult, ...launch } = {}) {
|
|
155
|
+
for (const capture of captures) {
|
|
156
|
+
assertCapturable(capture);
|
|
157
|
+
}
|
|
158
|
+
const results = [];
|
|
159
|
+
let browser;
|
|
160
|
+
// chained, so workers that find no live browser share one launch
|
|
161
|
+
const liveBrowser = () => {
|
|
162
|
+
const launched = () => launchBrowser(launch);
|
|
163
|
+
browser = browser
|
|
164
|
+
? browser.then(b => (b.connected ? b : launched()), launched)
|
|
165
|
+
: launched();
|
|
166
|
+
return browser;
|
|
167
|
+
};
|
|
168
|
+
let next = 0;
|
|
169
|
+
const worker = async () => {
|
|
170
|
+
while (next < captures.length) {
|
|
171
|
+
const index = next++;
|
|
172
|
+
const capture = captures[index];
|
|
173
|
+
const start = performance.now();
|
|
174
|
+
let result;
|
|
175
|
+
let page;
|
|
176
|
+
try {
|
|
177
|
+
const prepared = await prepareOpen(capture);
|
|
178
|
+
const opened = await openPage(await liveBrowser(), prepared);
|
|
179
|
+
page = opened.page;
|
|
180
|
+
const { url } = opened;
|
|
181
|
+
result = {
|
|
182
|
+
ok: true,
|
|
183
|
+
out: capture.out,
|
|
184
|
+
url,
|
|
185
|
+
...(await shoot(page, opened, capture)),
|
|
186
|
+
ms: performance.now() - start,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
result = {
|
|
191
|
+
ok: false,
|
|
192
|
+
out: capture.out,
|
|
193
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
194
|
+
ms: performance.now() - start,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
await page?.close().catch(() => { });
|
|
199
|
+
}
|
|
200
|
+
results[index] = result;
|
|
201
|
+
onResult?.(result, index);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
try {
|
|
205
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, captures.length) }, worker));
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
const launched = await browser?.catch(() => undefined);
|
|
209
|
+
await launched?.close().catch(() => { });
|
|
210
|
+
}
|
|
211
|
+
return results;
|
|
212
|
+
}
|
package/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { captureJBrowse, openJBrowse } from './capture.ts';
|
|
1
|
+
export { captureBatch, captureJBrowse, openJBrowse } from './capture.ts';
|
|
2
2
|
export { clearAnnotations, drawAnnotations } from './annotations.ts';
|
|
3
3
|
export { delay } from './poll.ts';
|
|
4
4
|
export { waitForFrame, waitForJBrowseReady } from './ready.ts';
|
|
@@ -9,7 +9,8 @@ export { PENDING_DISPLAYS, displayPainted, displaySettled, waitForAppSettled, wa
|
|
|
9
9
|
export { BASE_CHROME_ARGS, findChromeExecutable, isBrowserConsoleNoise, launchBrowser, } from './browser.ts';
|
|
10
10
|
export type { Annotation, AnnotationAnchor } from './annotationOverlay.ts';
|
|
11
11
|
export type { LaunchOptions } from './browser.ts';
|
|
12
|
-
export type { CaptureOptions, CaptureResult, OpenOptions, OpenResult, } from './capture.ts';
|
|
12
|
+
export type { BatchOptions, BatchResult, CaptureOptions, CaptureResult, OpenOptions, OpenResult, } from './capture.ts';
|
|
13
|
+
export type { PageOrFrame } from './poll.ts';
|
|
13
14
|
export type { ReadyOptions, ReadyReport } from './ready.ts';
|
|
14
15
|
export type { DisplayState, SessionExpectations } from './sessionGate.ts';
|
|
15
16
|
export type { JBrowseUrlOptions } from './url.ts';
|
package/esm/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { captureJBrowse, openJBrowse } from './capture.js';
|
|
1
|
+
export { captureBatch, captureJBrowse, openJBrowse } from './capture.js';
|
|
2
2
|
export { clearAnnotations, drawAnnotations } from './annotations.js';
|
|
3
3
|
export { delay } from './poll.js';
|
|
4
4
|
export { waitForFrame, waitForJBrowseReady } from './ready.js';
|
package/esm/jsonArgs.d.ts
CHANGED
|
@@ -2,3 +2,22 @@ import type { Annotation } from './annotationOverlay.ts';
|
|
|
2
2
|
export declare function readJson(flag: string, value: string): object;
|
|
3
3
|
export declare function readAnnotations(value: string): Annotation[];
|
|
4
4
|
export declare function readSpec(value: string): object;
|
|
5
|
+
export interface BatchEntry {
|
|
6
|
+
out: string;
|
|
7
|
+
hub?: string;
|
|
8
|
+
config?: string;
|
|
9
|
+
assembly?: string;
|
|
10
|
+
loc?: string;
|
|
11
|
+
tracks?: string[];
|
|
12
|
+
/** A session spec inline, or a path to one. */
|
|
13
|
+
spec?: object | string;
|
|
14
|
+
/** A session saved with File → Export session inline, or a path to one. */
|
|
15
|
+
session?: object | string;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
dpr?: number;
|
|
19
|
+
fullPage?: boolean;
|
|
20
|
+
/** Callouts inline, or a path to them. */
|
|
21
|
+
annotations?: Annotation[] | string;
|
|
22
|
+
}
|
|
23
|
+
export declare function readBatch(value: string): BatchEntry[];
|
package/esm/jsonArgs.js
CHANGED
|
@@ -10,7 +10,7 @@ function parseJson(flag, value) {
|
|
|
10
10
|
: readFileSync(value, 'utf8'));
|
|
11
11
|
}
|
|
12
12
|
catch (error) {
|
|
13
|
-
throw new Error(`--${flag}: ${error instanceof Error ? error.message : error}`, { cause: error });
|
|
13
|
+
throw new Error(`${flag.includes(' ') ? flag : `--${flag}`}: ${error instanceof Error ? error.message : error}`, { cause: error });
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
export function readJson(flag, value) {
|
|
@@ -32,3 +32,15 @@ export function readSpec(value) {
|
|
|
32
32
|
const spec = readJson('spec', value);
|
|
33
33
|
return 'type' in spec && !('views' in spec) ? { views: [spec] } : spec;
|
|
34
34
|
}
|
|
35
|
+
export function readBatch(value) {
|
|
36
|
+
const parsed = parseJson('batch manifest', value);
|
|
37
|
+
if (!Array.isArray(parsed)) {
|
|
38
|
+
throw new Error('the batch manifest must be a JSON array of captures');
|
|
39
|
+
}
|
|
40
|
+
for (const [i, entry] of parsed.entries()) {
|
|
41
|
+
if (typeof entry?.out !== 'string') {
|
|
42
|
+
throw new Error(`batch capture ${i} has no "out" image path`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
package/esm/poll.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { Page } from 'puppeteer';
|
|
1
|
+
import type { Frame, Page } from 'puppeteer';
|
|
2
|
+
/** A page, or a frame inside one such as an embedded JBrowse's iframe. */
|
|
3
|
+
export type PageOrFrame = Page | Frame;
|
|
2
4
|
/** The budget each stage of the ready chain gets unless told otherwise. */
|
|
3
5
|
export declare const DEFAULT_TIMEOUT = 60000;
|
|
4
6
|
export declare const delay: (ms: number) => Promise<void>;
|
|
@@ -18,4 +20,4 @@ export declare function holdTrue(read: () => Promise<boolean>, { holdMs, timeout
|
|
|
18
20
|
* poll can outlive, such as a navigation destroying the execution context. A
|
|
19
21
|
* closed or crashed page throws, since no poll will recover it.
|
|
20
22
|
*/
|
|
21
|
-
export declare function queryWhileOpen<T, F>(
|
|
23
|
+
export declare function queryWhileOpen<T, F>(target: PageOrFrame, query: Promise<T>, fallback: F): Promise<T | F>;
|
package/esm/poll.js
CHANGED
|
@@ -28,21 +28,24 @@ export async function holdTrue(read, { holdMs, timeout, pollMs, }) {
|
|
|
28
28
|
}
|
|
29
29
|
return false;
|
|
30
30
|
}
|
|
31
|
+
function isGone(target) {
|
|
32
|
+
return 'page' in target
|
|
33
|
+
? target.detached || isGone(target.page())
|
|
34
|
+
: target.isClosed() || !target.browser().connected;
|
|
35
|
+
}
|
|
31
36
|
const PAGE_GONE = /crashed|Target closed|Session closed|Connection closed/i;
|
|
32
37
|
/**
|
|
33
38
|
* Await a page query, answering `fallback` when it failed for a reason a later
|
|
34
39
|
* poll can outlive, such as a navigation destroying the execution context. A
|
|
35
40
|
* closed or crashed page throws, since no poll will recover it.
|
|
36
41
|
*/
|
|
37
|
-
export async function queryWhileOpen(
|
|
42
|
+
export async function queryWhileOpen(target, query, fallback) {
|
|
38
43
|
try {
|
|
39
44
|
return await query;
|
|
40
45
|
}
|
|
41
46
|
catch (error) {
|
|
42
47
|
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
-
if (PAGE_GONE.test(message) ||
|
|
44
|
-
page.isClosed() ||
|
|
45
|
-
!page.browser().connected) {
|
|
48
|
+
if (PAGE_GONE.test(message) || isGone(target)) {
|
|
46
49
|
throw new Error(`the page closed or crashed before it finished rendering (${message})`, { cause: error });
|
|
47
50
|
}
|
|
48
51
|
return fallback;
|
package/esm/ready.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { PageOrFrame } from './poll.ts';
|
|
1
2
|
import type { DisplayState, SessionExpectations } from './sessionGate.ts';
|
|
2
|
-
import type { Page } from 'puppeteer';
|
|
3
3
|
export interface ReadyOptions extends SessionExpectations {
|
|
4
4
|
/** Budget for each wait stage. */
|
|
5
5
|
timeout?: number;
|
|
@@ -21,9 +21,9 @@ export interface ReadyReport {
|
|
|
21
21
|
* Wait until the session holds the assembly and tracks asked for, the app has
|
|
22
22
|
* held itself ready, every display has painted, and no error is on screen.
|
|
23
23
|
*/
|
|
24
|
-
export declare function waitForJBrowseReady(page:
|
|
24
|
+
export declare function waitForJBrowseReady(page: PageOrFrame, options?: ReadyOptions): Promise<ReadyReport>;
|
|
25
25
|
/**
|
|
26
26
|
* `waitForJBrowseReady` without the session gate, for a frame that changed
|
|
27
27
|
* while the session did not, such as after a viewport resize.
|
|
28
28
|
*/
|
|
29
|
-
export declare function waitForFrame(page:
|
|
29
|
+
export declare function waitForFrame(page: PageOrFrame, { timeout, allowUnsettled, }?: Omit<ReadyOptions, keyof SessionExpectations>): Promise<ReadyReport>;
|
package/esm/sessionGate.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PageOrFrame } from './poll.ts';
|
|
2
2
|
export interface SessionExpectations {
|
|
3
3
|
/** Assembly that must be open on some view. */
|
|
4
4
|
assembly?: string;
|
|
@@ -30,7 +30,7 @@ export declare function readSessionInPage(): {
|
|
|
30
30
|
* Throws at once with the app's message if it could not load, and on timeout
|
|
31
31
|
* with what the census held instead.
|
|
32
32
|
*/
|
|
33
|
-
export declare function waitForSession(page:
|
|
33
|
+
export declare function waitForSession(page: PageOrFrame, { assembly, trackIds, minViews, timeout, }?: SessionExpectations & {
|
|
34
34
|
timeout?: number;
|
|
35
35
|
}): Promise<void>;
|
|
36
36
|
/** One display and the phase it publishes. */
|
package/esm/url.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const PUBLIC_INSTANCE = "https://jbrowse.org/code/jb2/v5.0.0-beta.
|
|
1
|
+
export declare const PUBLIC_INSTANCE = "https://jbrowse.org/code/jb2/v5.0.0-beta.14/";
|
|
2
2
|
/**
|
|
3
3
|
* The instance as the page that loads it sees it: a directory given without
|
|
4
4
|
* its slash, which the host redirects, resolves relative URLs one level up.
|
package/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "5.0.0-beta.
|
|
1
|
+
export declare const version = "5.0.0-beta.14";
|
package/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '5.0.0-beta.
|
|
1
|
+
export const version = '5.0.0-beta.14';
|
package/esm/waits.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PageOrFrame } from './poll.ts';
|
|
2
|
+
import type { ElementHandle } from 'puppeteer';
|
|
2
3
|
interface WaitOptions {
|
|
3
4
|
timeout?: number;
|
|
4
5
|
}
|
|
@@ -14,7 +15,7 @@ export declare const displaySettled: (testid: string) => string;
|
|
|
14
15
|
* `page.waitForSelector`, whose timeout names each display that had not
|
|
15
16
|
* painted and the phase it was in.
|
|
16
17
|
*/
|
|
17
|
-
export declare function waitForSelectorAttributed(page:
|
|
18
|
+
export declare function waitForSelectorAttributed(page: PageOrFrame, selector: string, { timeout }?: WaitOptions): Promise<ElementHandle>;
|
|
18
19
|
type SettleOptions = WaitOptions & {
|
|
19
20
|
holdMs?: number;
|
|
20
21
|
pollMs?: number;
|
|
@@ -23,7 +24,7 @@ type SettleOptions = WaitOptions & {
|
|
|
23
24
|
* `waitForAppSettled`, answering with what was still blocking at the timeout,
|
|
24
25
|
* or undefined once the hold passed.
|
|
25
26
|
*/
|
|
26
|
-
export declare function appSettledBlocker(page:
|
|
27
|
+
export declare function appSettledBlocker(page: PageOrFrame, { timeout, holdMs, pollMs, }?: SettleOptions): Promise<string | undefined>;
|
|
27
28
|
/**
|
|
28
29
|
* Wait until every app on the page has read ready, with nothing loading or
|
|
29
30
|
* animating, for an unbroken `holdMs`. False on timeout. Right after a
|
|
@@ -31,5 +32,5 @@ export declare function appSettledBlocker(page: Page, { timeout, holdMs, pollMs,
|
|
|
31
32
|
* publishes `[data-app-phase]` throws, since nothing positive exists to wait
|
|
32
33
|
* for.
|
|
33
34
|
*/
|
|
34
|
-
export declare function waitForAppSettled(page:
|
|
35
|
+
export declare function waitForAppSettled(page: PageOrFrame, options?: SettleOptions): Promise<boolean>;
|
|
35
36
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/capture",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.14",
|
|
4
4
|
"description": "Drive a JBrowse 2 instance with Puppeteer and screenshot it once it has actually finished rendering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jbrowse",
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
],
|
|
34
34
|
"sideEffects": false,
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"puppeteer": "^25.
|
|
36
|
+
"puppeteer": "^25.14.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@jbrowse/core": "^5.0.0-beta.
|
|
39
|
+
"@jbrowse/core": "^5.0.0-beta.14"
|
|
40
40
|
},
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public"
|