@opendatalabs/darshana 1.2.0 → 1.3.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 +5 -0
- package/package.json +2 -2
- package/src/capture.mjs +11 -6
- package/src/config.mjs +31 -0
- package/src/index.mjs +238 -74
package/README.md
CHANGED
|
@@ -27,6 +27,11 @@ npm install -g @opendatalabs/darshana
|
|
|
27
27
|
|
|
28
28
|
Chromium is installed automatically. Or skip the install entirely and use `npx @opendatalabs/darshana`.
|
|
29
29
|
|
|
30
|
+
**Linux only:** if Chromium fails to launch, you may be missing system libraries. Fix with:
|
|
31
|
+
```bash
|
|
32
|
+
sudo npx playwright install chromium --with-deps
|
|
33
|
+
```
|
|
34
|
+
|
|
30
35
|
## CLI reference
|
|
31
36
|
|
|
32
37
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendatalabs/darshana",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Crawl any web app and generate a labeled PDF, HTML viewer, or image set for design review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"path-to-regexp": "^8.0.0"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"postinstall": "playwright install chromium
|
|
20
|
+
"postinstall": "playwright install chromium"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"semantic-release": "^25.0.0",
|
package/src/capture.mjs
CHANGED
|
@@ -7,6 +7,8 @@ export async function captureAll(browser, config, urls) {
|
|
|
7
7
|
const results = [];
|
|
8
8
|
const themes = config.capture.themes;
|
|
9
9
|
const viewportNames = config.capture.viewports;
|
|
10
|
+
const total = urls.length * themes.length * viewportNames.length;
|
|
11
|
+
const counter = { n: 0 };
|
|
10
12
|
|
|
11
13
|
for (const viewportName of viewportNames) {
|
|
12
14
|
for (const theme of themes) {
|
|
@@ -20,7 +22,7 @@ export async function captureAll(browser, config, urls) {
|
|
|
20
22
|
...(storageStatePath ? { storageState: storageStatePath } : {}),
|
|
21
23
|
};
|
|
22
24
|
|
|
23
|
-
console.log(`\n[capture]
|
|
25
|
+
console.log(`\n[capture] Starting segment ${viewportName}/${theme} (${urls.length} URLs)`);
|
|
24
26
|
const context = await browser.newContext(contextOpts);
|
|
25
27
|
|
|
26
28
|
for (const url of urls) {
|
|
@@ -31,20 +33,23 @@ export async function captureAll(browser, config, urls) {
|
|
|
31
33
|
|
|
32
34
|
const effectiveThemes = override?.themes;
|
|
33
35
|
if (effectiveThemes && !effectiveThemes.includes(theme)) {
|
|
34
|
-
|
|
36
|
+
counter.n++;
|
|
37
|
+
console.log(` [capture] ${counter.n}/${total} skip ${pathname} [${theme}] (override)`);
|
|
35
38
|
continue;
|
|
36
39
|
}
|
|
37
40
|
const effectiveViewports = override?.viewports;
|
|
38
41
|
if (effectiveViewports && !effectiveViewports.includes(viewportName)) {
|
|
39
|
-
|
|
42
|
+
counter.n++;
|
|
43
|
+
console.log(` [capture] ${counter.n}/${total} skip ${pathname} [${viewportName}] (override)`);
|
|
40
44
|
continue;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
const delay = override?.delay ?? config.capture.delay ?? 400;
|
|
44
48
|
const waitFor = override?.waitFor ?? config.capture.waitFor ?? null;
|
|
45
49
|
const label = makeLabel(pathname, viewportName, theme);
|
|
50
|
+
counter.n++;
|
|
46
51
|
|
|
47
|
-
console.log(` [capture] ${label}`);
|
|
52
|
+
console.log(` [capture] ${counter.n}/${total} ${label}`);
|
|
48
53
|
const page = await context.newPage();
|
|
49
54
|
|
|
50
55
|
try {
|
|
@@ -96,14 +101,14 @@ export async function captureAll(browser, config, urls) {
|
|
|
96
101
|
|
|
97
102
|
results.push({ url, pathname, theme, viewport: viewportName, imageBuffer, label });
|
|
98
103
|
} catch (err) {
|
|
99
|
-
console.error(` [capture] FAILED ${pathname}: ${err.message}`);
|
|
104
|
+
console.error(` [capture] ${counter.n}/${total} FAILED ${pathname}: ${err.message}`);
|
|
100
105
|
} finally {
|
|
101
106
|
await page.close();
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
await context.close();
|
|
106
|
-
console.log(`[capture] Segment done: ${viewportName}
|
|
111
|
+
console.log(`[capture] Segment done: ${viewportName}/${theme}`);
|
|
107
112
|
}
|
|
108
113
|
}
|
|
109
114
|
|
package/src/config.mjs
CHANGED
|
@@ -6,6 +6,33 @@ export const VIEWPORT_PRESETS = {
|
|
|
6
6
|
mobile: { width: 390, height: 844, deviceScaleFactor: 2 },
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
const KNOWN_TOP_LEVEL_KEYS = ['title', 'url', 'start', 'public', 'authStorage', 'authScript', 'crawl', 'capture', 'pdf', 'outputs', 'outputDir'];
|
|
10
|
+
const KNOWN_CRAWL_KEYS = ['include', 'exclude', 'maxDepth', 'maxPages', 'extraRoutes', 'routes'];
|
|
11
|
+
const KNOWN_CAPTURE_KEYS = ['themes', 'viewports', 'fullPage', 'delay', 'waitFor', 'contextOptions', 'launchOptions', 'playwrightOptions', 'routeOptions', 'overrides'];
|
|
12
|
+
|
|
13
|
+
function levenshtein(a, b) {
|
|
14
|
+
const m = a.length, n = b.length;
|
|
15
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
|
|
16
|
+
for (let i = 1; i <= m; i++) {
|
|
17
|
+
for (let j = 1; j <= n; j++) {
|
|
18
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return dp[m][n];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function warnUnknownKeys(obj, knownKeys, section) {
|
|
25
|
+
if (!obj || typeof obj !== 'object') return;
|
|
26
|
+
for (const key of Object.keys(obj)) {
|
|
27
|
+
if (!knownKeys.includes(key)) {
|
|
28
|
+
const best = knownKeys.reduce((acc, k) => { const d = levenshtein(key, k); return d < acc.d ? { k, d } : acc; }, { k: null, d: Infinity });
|
|
29
|
+
const prefix = section ? `[darshana] Warning: unknown config field "${section}.${key}"` : `[darshana] Warning: unknown config field "${key}"`;
|
|
30
|
+
const suffix = best.d <= 3 ? ` — did you mean "${section ? section + '.' : ''}${best.k}"?` : '';
|
|
31
|
+
console.warn(prefix + suffix);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
9
36
|
export function loadConfig(configPath) {
|
|
10
37
|
const absConfigPath = path.resolve(configPath);
|
|
11
38
|
const configDir = path.dirname(absConfigPath);
|
|
@@ -25,6 +52,10 @@ export function loadConfig(configPath) {
|
|
|
25
52
|
// Build a config object from a plain object (used by both loadConfig and CLI --url mode).
|
|
26
53
|
// configDir is used to resolve relative paths; defaults to cwd when not loading from a file.
|
|
27
54
|
export function buildConfig(raw, configDir = process.cwd()) {
|
|
55
|
+
// Validate unknown keys and emit typo hints
|
|
56
|
+
warnUnknownKeys(raw, KNOWN_TOP_LEVEL_KEYS, null);
|
|
57
|
+
warnUnknownKeys(raw.crawl, KNOWN_CRAWL_KEYS, 'crawl');
|
|
58
|
+
warnUnknownKeys(raw.capture, KNOWN_CAPTURE_KEYS, 'capture');
|
|
28
59
|
const config = {
|
|
29
60
|
title: raw.title ?? 'Design Review',
|
|
30
61
|
url: raw.url.replace(/\/$/, ''),
|
package/src/index.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import readline from 'node:readline';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
4
6
|
import { chromium } from 'playwright';
|
|
5
7
|
import { loadConfig, buildConfig } from './config.mjs';
|
|
6
8
|
import { ensureAuth } from './auth.mjs';
|
|
@@ -9,6 +11,9 @@ import { captureAll } from './capture.mjs';
|
|
|
9
11
|
import { assemblePdf } from './pdf.mjs';
|
|
10
12
|
import { assembleHtml } from './html.mjs';
|
|
11
13
|
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const pkg = require('../package.json');
|
|
16
|
+
|
|
12
17
|
const USAGE = `
|
|
13
18
|
Usage:
|
|
14
19
|
darshana --url <url> [options] # zero-config mode
|
|
@@ -36,6 +41,155 @@ Options:
|
|
|
36
41
|
--auth-only Save auth session and exit
|
|
37
42
|
`.trim();
|
|
38
43
|
|
|
44
|
+
// ── Config summary box ────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function printConfigSummary(config) {
|
|
47
|
+
const INNER = 48;
|
|
48
|
+
const authLabel = config.public
|
|
49
|
+
? 'public'
|
|
50
|
+
: config.authScript
|
|
51
|
+
? `script: ${path.basename(config.authScript)}`
|
|
52
|
+
: 'session file';
|
|
53
|
+
|
|
54
|
+
const rows = [
|
|
55
|
+
['url', config.url],
|
|
56
|
+
['start', config.start],
|
|
57
|
+
['auth', authLabel],
|
|
58
|
+
['themes', (config.capture.themes ?? ['system']).join(', ')],
|
|
59
|
+
['viewports', (config.capture.viewports ?? ['desktop']).join(', ')],
|
|
60
|
+
['maxPages', `${config.crawl.maxPages} maxDepth ${config.crawl.maxDepth}`],
|
|
61
|
+
['outputs', (config.outputs ?? ['pdf', 'html']).join(', ')],
|
|
62
|
+
['outputDir', config.outputDir ?? './darshana-output'],
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Compute column widths
|
|
66
|
+
const keyWidth = Math.max(...rows.map(([k]) => k.length));
|
|
67
|
+
const valWidth = INNER - keyWidth - 2; // 2 = space + space
|
|
68
|
+
|
|
69
|
+
function trunc(s, max) {
|
|
70
|
+
const str = String(s);
|
|
71
|
+
return str.length > max ? str.slice(0, max - 1) + '…' : str;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pad(s, len) { return s + ' '.repeat(Math.max(0, len - s.length)); }
|
|
75
|
+
|
|
76
|
+
const topLabel = ' darshana ';
|
|
77
|
+
const topLineLen = INNER + 2; // borders
|
|
78
|
+
const dashLen = topLineLen - topLabel.length - 2; // -2 for corner chars
|
|
79
|
+
const dashLeft = Math.floor(dashLen / 2);
|
|
80
|
+
const dashRight = dashLen - dashLeft;
|
|
81
|
+
|
|
82
|
+
console.log(`┌${'─'.repeat(dashLeft)}${topLabel}${'─'.repeat(dashRight)}┐`);
|
|
83
|
+
for (const [k, v] of rows) {
|
|
84
|
+
const paddedKey = pad(k, keyWidth);
|
|
85
|
+
const truncVal = trunc(v, valWidth);
|
|
86
|
+
const paddedVal = pad(truncVal, valWidth);
|
|
87
|
+
console.log(`│ ${paddedKey} ${paddedVal} │`);
|
|
88
|
+
}
|
|
89
|
+
console.log(`└${'─'.repeat(INNER)}┘`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── init subcommand ───────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
async function runInit() {
|
|
95
|
+
const configPath = path.join(process.cwd(), 'review.config.json');
|
|
96
|
+
|
|
97
|
+
// Build an `ask` function that works for both TTY and piped stdin.
|
|
98
|
+
// For piped input: readline fires 'close' as soon as the pipe ends, breaking
|
|
99
|
+
// sequential question() calls. We buffer all lines upfront and serve them.
|
|
100
|
+
// For interactive TTY: use readline.question() normally.
|
|
101
|
+
const ask = await makeAsker();
|
|
102
|
+
|
|
103
|
+
if (fs.existsSync(configPath)) {
|
|
104
|
+
const overwrite = await ask('review.config.json already exists. Overwrite? (y/N): ');
|
|
105
|
+
if (overwrite.trim().toLowerCase() !== 'y') {
|
|
106
|
+
console.log('Aborted.');
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 1. URL
|
|
112
|
+
let url;
|
|
113
|
+
while (true) {
|
|
114
|
+
const raw = await ask('App URL (e.g. https://myapp.example.com): ');
|
|
115
|
+
try { url = new URL(raw.trim()).href.replace(/\/$/, ''); break; }
|
|
116
|
+
catch { console.log(' Invalid URL. Please include the scheme (https://).'); }
|
|
117
|
+
}
|
|
118
|
+
const hostname = new URL(url).hostname;
|
|
119
|
+
|
|
120
|
+
// 2. Title
|
|
121
|
+
const titleRaw = await ask(`Title (default: ${hostname}): `);
|
|
122
|
+
const title = titleRaw.trim() || hostname;
|
|
123
|
+
|
|
124
|
+
// 3. Auth
|
|
125
|
+
const authRaw = await ask('Is the app public? (y/N): ');
|
|
126
|
+
const isPublic = authRaw.trim().toLowerCase() === 'y';
|
|
127
|
+
|
|
128
|
+
// 4. Start path
|
|
129
|
+
const startRaw = await ask('Start path (default: /): ');
|
|
130
|
+
const start = startRaw.trim() || '/';
|
|
131
|
+
|
|
132
|
+
// 5. Viewports
|
|
133
|
+
const vpRaw = await ask('Viewports — desktop, mobile, or both? (default: desktop): ');
|
|
134
|
+
const vpInput = vpRaw.trim().toLowerCase() || 'desktop';
|
|
135
|
+
let viewports;
|
|
136
|
+
if (vpInput === 'both') viewports = ['desktop', 'mobile'];
|
|
137
|
+
else if (vpInput === 'mobile') viewports = ['mobile'];
|
|
138
|
+
else viewports = ['desktop'];
|
|
139
|
+
|
|
140
|
+
// 6. Themes
|
|
141
|
+
const themeRaw = await ask('Themes — system, dark, light, or multiple? (default: system): ');
|
|
142
|
+
const themeInput = themeRaw.trim() || 'system';
|
|
143
|
+
let themes;
|
|
144
|
+
if (themeInput === 'multiple' || themeInput.includes(',')) {
|
|
145
|
+
themes = themeInput === 'multiple'
|
|
146
|
+
? ['dark', 'light']
|
|
147
|
+
: themeInput.split(',').map(s => s.trim()).filter(Boolean);
|
|
148
|
+
} else {
|
|
149
|
+
themes = [themeInput];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 7. Output dir
|
|
153
|
+
const outRaw = await ask('Output directory (default: ./darshana-output): ');
|
|
154
|
+
const outputDir = outRaw.trim() || './darshana-output';
|
|
155
|
+
|
|
156
|
+
const config = {
|
|
157
|
+
title,
|
|
158
|
+
url,
|
|
159
|
+
start,
|
|
160
|
+
public: isPublic,
|
|
161
|
+
capture: { viewports, themes },
|
|
162
|
+
outputDir,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
166
|
+
console.log('✓ review.config.json created. Run: darshana --config review.config.json');
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Returns an `ask(question)` function.
|
|
171
|
+
// TTY mode: wraps readline.question for interactive line editing.
|
|
172
|
+
// Piped mode: buffers all stdin lines first, then serves them sequentially.
|
|
173
|
+
async function makeAsker() {
|
|
174
|
+
if (process.stdin.isTTY) {
|
|
175
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
176
|
+
return (q) => new Promise(resolve => rl.question(q, answer => { resolve(answer); }));
|
|
177
|
+
}
|
|
178
|
+
// Non-TTY: read all lines from stdin before asking anything
|
|
179
|
+
const lines = await new Promise(resolve => {
|
|
180
|
+
const buf = [];
|
|
181
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
182
|
+
rl.on('line', l => buf.push(l));
|
|
183
|
+
rl.on('close', () => resolve(buf));
|
|
184
|
+
});
|
|
185
|
+
let idx = 0;
|
|
186
|
+
return (q) => {
|
|
187
|
+
const answer = lines[idx++] ?? '';
|
|
188
|
+
console.log(`${q}${answer}`);
|
|
189
|
+
return Promise.resolve(answer);
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
39
193
|
function parseArgs(argv) {
|
|
40
194
|
const args = {
|
|
41
195
|
config: null,
|
|
@@ -82,6 +236,7 @@ function parseArgs(argv) {
|
|
|
82
236
|
if (a === '--route') { args.route = next(); continue; }
|
|
83
237
|
if (a === '--auth-only') { args.authOnly = true; continue; }
|
|
84
238
|
if (a === '--help' || a === '-h') { console.log(USAGE); process.exit(0); }
|
|
239
|
+
if (a === '--version' || a === '-v') { console.log(`darshana ${pkg.version}`); process.exit(0); }
|
|
85
240
|
console.error(`Unknown argument: ${a}\n\n${USAGE}`);
|
|
86
241
|
process.exit(1);
|
|
87
242
|
}
|
|
@@ -136,95 +291,109 @@ function applyCliOverrides(config, args) {
|
|
|
136
291
|
return config;
|
|
137
292
|
}
|
|
138
293
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
294
|
+
// Dispatch init before parseArgs so we don't need --url
|
|
295
|
+
if (process.argv[2] === 'init') {
|
|
296
|
+
runInit();
|
|
297
|
+
} else {
|
|
298
|
+
runMain();
|
|
144
299
|
}
|
|
145
300
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
config = configFromArgs(args);
|
|
301
|
+
function runMain() {
|
|
302
|
+
const args = parseArgs(process.argv.slice(2));
|
|
303
|
+
|
|
304
|
+
if (!args.config && !args.url) {
|
|
305
|
+
console.error('Error: --url or --config is required\n\n' + USAGE);
|
|
306
|
+
process.exit(1);
|
|
153
307
|
}
|
|
154
308
|
|
|
155
|
-
|
|
309
|
+
async function main() {
|
|
310
|
+
let config;
|
|
311
|
+
if (args.config) {
|
|
312
|
+
config = loadConfig(args.config);
|
|
313
|
+
config = applyCliOverrides(config, args);
|
|
314
|
+
} else {
|
|
315
|
+
config = configFromArgs(args);
|
|
316
|
+
}
|
|
156
317
|
|
|
157
|
-
|
|
158
|
-
|
|
318
|
+
const storageStatePath = await ensureAuth(config);
|
|
319
|
+
config._storageStatePath = storageStatePath;
|
|
159
320
|
|
|
160
|
-
|
|
161
|
-
console.log('[darshana] --auth-only done.');
|
|
162
|
-
process.exit(0);
|
|
163
|
-
}
|
|
321
|
+
printConfigSummary(config);
|
|
164
322
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
urls = [fullUrl];
|
|
169
|
-
console.log(`[darshana] --route mode: ${fullUrl}`);
|
|
170
|
-
} else {
|
|
171
|
-
const crawlBrowser = await chromium.launch({ headless: true });
|
|
172
|
-
const crawlContextOpts = storageStatePath ? { storageState: storageStatePath } : {};
|
|
173
|
-
const crawlContext = await crawlBrowser.newContext(crawlContextOpts);
|
|
174
|
-
try {
|
|
175
|
-
console.log(`[darshana] Crawling from ${config.url}${config.start} ...`);
|
|
176
|
-
urls = await crawl(crawlContext, config);
|
|
177
|
-
} finally {
|
|
178
|
-
await crawlContext.close();
|
|
179
|
-
await crawlBrowser.close();
|
|
323
|
+
if (args.authOnly) {
|
|
324
|
+
console.log('[darshana] --auth-only done.');
|
|
325
|
+
process.exit(0);
|
|
180
326
|
}
|
|
181
|
-
console.log(`[darshana] Crawl complete: ${urls.length} URLs found.`);
|
|
182
|
-
}
|
|
183
327
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
328
|
+
let urls;
|
|
329
|
+
if (args.route) {
|
|
330
|
+
const fullUrl = args.route.startsWith('http') ? args.route : config.url + args.route;
|
|
331
|
+
urls = [fullUrl];
|
|
332
|
+
console.log(`[darshana] --route mode: ${fullUrl}`);
|
|
333
|
+
} else {
|
|
334
|
+
const crawlBrowser = await chromium.launch({ headless: true });
|
|
335
|
+
const crawlContextOpts = storageStatePath ? { storageState: storageStatePath } : {};
|
|
336
|
+
const crawlContext = await crawlBrowser.newContext(crawlContextOpts);
|
|
337
|
+
try {
|
|
338
|
+
console.log(`[darshana] Crawling from ${config.url}${config.start} ...`);
|
|
339
|
+
urls = await crawl(crawlContext, config);
|
|
340
|
+
} finally {
|
|
341
|
+
await crawlContext.close();
|
|
342
|
+
await crawlBrowser.close();
|
|
343
|
+
}
|
|
344
|
+
console.log(`[darshana] Crawl complete: ${urls.length} URLs found.`);
|
|
345
|
+
}
|
|
189
346
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
347
|
+
if (args.dryRun) {
|
|
348
|
+
console.log('\n[darshana] --dry-run: discovered URLs:');
|
|
349
|
+
for (const u of urls) console.log(` ${u}`);
|
|
350
|
+
process.exit(0);
|
|
351
|
+
}
|
|
195
352
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
353
|
+
const browser = await chromium.launch({
|
|
354
|
+
headless: true,
|
|
355
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
356
|
+
...(config.capture.launchOptions ?? {}),
|
|
357
|
+
});
|
|
202
358
|
|
|
203
|
-
|
|
359
|
+
let captures;
|
|
360
|
+
try {
|
|
361
|
+
captures = await captureAll(browser, config, urls);
|
|
362
|
+
} finally {
|
|
363
|
+
await browser.close();
|
|
364
|
+
}
|
|
204
365
|
|
|
205
|
-
|
|
206
|
-
console.error('[darshana] No pages captured. Exiting.');
|
|
207
|
-
process.exit(1);
|
|
208
|
-
}
|
|
366
|
+
console.log(`\n[darshana] Captured ${captures.length} page(s).`);
|
|
209
367
|
|
|
210
|
-
|
|
211
|
-
|
|
368
|
+
if (captures.length === 0) {
|
|
369
|
+
console.error('[darshana] No pages captured. Exiting.');
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
212
372
|
|
|
213
|
-
|
|
373
|
+
const outputDir = config.outputDir;
|
|
374
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
214
375
|
|
|
215
|
-
|
|
216
|
-
await assemblePdf(captures, config, outputDir);
|
|
217
|
-
}
|
|
376
|
+
const outputs = config.outputs ?? ['pdf', 'html'];
|
|
218
377
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
378
|
+
if (outputs.includes('pdf')) {
|
|
379
|
+
await assemblePdf(captures, config, outputDir);
|
|
380
|
+
}
|
|
222
381
|
|
|
223
|
-
|
|
224
|
-
|
|
382
|
+
if (outputs.includes('html')) {
|
|
383
|
+
await assembleHtml(captures, config, outputDir);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (outputs.includes('images')) {
|
|
387
|
+
await writeImages(captures, outputDir);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
console.log('\n[darshana] Done.');
|
|
225
391
|
}
|
|
226
392
|
|
|
227
|
-
|
|
393
|
+
main().catch(err => {
|
|
394
|
+
console.error('[darshana] Fatal:', err);
|
|
395
|
+
process.exit(1);
|
|
396
|
+
});
|
|
228
397
|
}
|
|
229
398
|
|
|
230
399
|
async function writeImages(captures, outputDir) {
|
|
@@ -257,8 +426,3 @@ function slugifyPathname(pathname) {
|
|
|
257
426
|
.replace(/^-|-$/g, '')
|
|
258
427
|
|| 'root';
|
|
259
428
|
}
|
|
260
|
-
|
|
261
|
-
main().catch(err => {
|
|
262
|
-
console.error('[darshana] Fatal:', err);
|
|
263
|
-
process.exit(1);
|
|
264
|
-
});
|