@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 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:
@@ -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 colour (a "light green/orange" highlight); otherwise hollow
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
- if (command !== 'capture') {
80
- for (const token of tokens) {
81
- if (token.kind === 'option' &&
82
- !SUBCOMMAND_FLAGS[command].has(token.name)) {
83
- throw new Error(`${token.rawName} does not apply to \`jb2capture ${command}\``);
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
- const browser = await launchBrowser({ headless, executablePath, args });
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
- const page = await browser.newPage();
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
- if (response && !response.ok()) {
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
- ...options,
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 browser.close().catch(() => { });
87
+ await page.close().catch(() => { });
83
88
  throw error;
84
89
  }
85
90
  }
86
91
  /**
87
- * Open a JBrowse session, wait for it to render, screenshot it, and close the
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 captureJBrowse(options = {}) {
91
- const { out, fullPage = false, annotations, ...openOptions } = options;
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
- const { browser, page, url, ...opened } = await openJBrowse(openOptions);
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
- let report = opened;
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>(page: Page, query: Promise<T>, fallback: F): Promise<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(page, query, fallback) {
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: Page, options?: ReadyOptions): Promise<ReadyReport>;
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: Page, { timeout, allowUnsettled, }?: Omit<ReadyOptions, keyof SessionExpectations>): Promise<ReadyReport>;
29
+ export declare function waitForFrame(page: PageOrFrame, { timeout, allowUnsettled, }?: Omit<ReadyOptions, keyof SessionExpectations>): Promise<ReadyReport>;
@@ -1,4 +1,4 @@
1
- import type { Page } from 'puppeteer';
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: Page, { assembly, trackIds, minViews, timeout, }?: SessionExpectations & {
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.13/";
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.13";
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.13';
1
+ export const version = '5.0.0-beta.14';
package/esm/waits.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { ElementHandle, Page } from 'puppeteer';
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: Page, selector: string, { timeout }?: WaitOptions): Promise<ElementHandle>;
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: Page, { timeout, holdMs, pollMs, }?: SettleOptions): Promise<string | undefined>;
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: Page, options?: SettleOptions): Promise<boolean>;
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.13",
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.12.0"
36
+ "puppeteer": "^25.14.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@jbrowse/core": "^5.0.0-beta.13"
39
+ "@jbrowse/core": "^5.0.0-beta.14"
40
40
  },
41
41
  "publishConfig": {
42
42
  "access": "public"