@link-assistant/web-capture 1.1.2
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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +294 -0
- package/.jscpd.json +20 -0
- package/.lenv +8 -0
- package/.prettierignore +7 -0
- package/.prettierrc +10 -0
- package/ARCHITECTURE.md +982 -0
- package/CHANGELOG.md +22 -0
- package/Dockerfile +45 -0
- package/LICENSE +24 -0
- package/README.md +268 -0
- package/apply.sh +166 -0
- package/babel.config.cjs +13 -0
- package/bin/web-capture.js +285 -0
- package/docker-compose.yml +9 -0
- package/docs/market-analysis-2025-05-13.md +64 -0
- package/eslint.config.js +112 -0
- package/examples/js/engine_comparison.js +39 -0
- package/examples/js/html_download.js +23 -0
- package/examples/js/image_download.js +36 -0
- package/examples/js/markdown_download.js +23 -0
- package/examples/js/playwright_example.js +23 -0
- package/examples/python/html_download.py +15 -0
- package/examples/python/image_download.py +20 -0
- package/examples/python/markdown_download.py +15 -0
- package/examples/python/playwright_example.py +22 -0
- package/jest.config.mjs +18 -0
- package/package.json +83 -0
- package/scripts/changeset-version.mjs +38 -0
- package/scripts/create-github-release.mjs +93 -0
- package/scripts/create-manual-changeset.mjs +88 -0
- package/scripts/format-github-release.mjs +83 -0
- package/scripts/format-release-notes.mjs +218 -0
- package/scripts/instant-version-bump.mjs +121 -0
- package/scripts/publish-to-npm.mjs +128 -0
- package/scripts/setup-npm.mjs +37 -0
- package/scripts/validate-changeset.mjs +106 -0
- package/scripts/version-and-commit.mjs +237 -0
- package/src/browser.js +207 -0
- package/src/fetch.js +38 -0
- package/src/html.js +64 -0
- package/src/image.js +41 -0
- package/src/index.js +55 -0
- package/src/lib.js +330 -0
- package/src/markdown.js +17 -0
- package/src/stream.js +50 -0
- package/tests/e2e/docker.test.js +148 -0
- package/tests/e2e/process.test.js +117 -0
- package/tests/integration/browser-engines.test.js +128 -0
- package/tests/jest.setup.mjs +7 -0
- package/tests/mock/index.test.js +223 -0
- package/tests/unit/browser.test.js +136 -0
- package/tests/unit/cli.test.js +151 -0
- package/tests/unit/convertRelativeUrls.test.js +34 -0
- package/tests/unit/html2md.test.js +610 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CLI entry point for web-capture
|
|
3
|
+
// Supports two modes:
|
|
4
|
+
// 1. Server mode: web-capture --serve [--port 3000]
|
|
5
|
+
// 2. Capture mode: web-capture <url> [options]
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import { URL } from 'node:url';
|
|
9
|
+
import { makeConfig } from 'lino-arguments';
|
|
10
|
+
|
|
11
|
+
// Create configuration using lino-arguments pattern
|
|
12
|
+
const config = makeConfig({
|
|
13
|
+
yargs: ({ yargs, getenv }) =>
|
|
14
|
+
yargs
|
|
15
|
+
.usage(
|
|
16
|
+
'web-capture - Capture web pages as HTML, Markdown, or PNG\n\nUsage:\n web-capture --serve [--port <port>] Start as API server\n web-capture <url> [options] Capture a URL to file/stdout'
|
|
17
|
+
)
|
|
18
|
+
.option('serve', {
|
|
19
|
+
alias: 's',
|
|
20
|
+
type: 'boolean',
|
|
21
|
+
description: 'Start as HTTP API server',
|
|
22
|
+
default: false,
|
|
23
|
+
})
|
|
24
|
+
.option('port', {
|
|
25
|
+
alias: 'p',
|
|
26
|
+
type: 'number',
|
|
27
|
+
description: 'Port to listen on (default: 3000, or PORT env)',
|
|
28
|
+
// Don't use getenv here - let yargs read from process.env.PORT directly
|
|
29
|
+
// which will be set by the test or by lino-arguments from .lenv
|
|
30
|
+
default: 3000,
|
|
31
|
+
})
|
|
32
|
+
.option('format', {
|
|
33
|
+
alias: 'f',
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Output format: html, markdown, md, image, png',
|
|
36
|
+
default: 'html',
|
|
37
|
+
})
|
|
38
|
+
.option('output', {
|
|
39
|
+
alias: 'o',
|
|
40
|
+
type: 'string',
|
|
41
|
+
description:
|
|
42
|
+
'Output file path (default: stdout for text, auto-generated for images)',
|
|
43
|
+
})
|
|
44
|
+
.option('engine', {
|
|
45
|
+
alias: 'e',
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Browser engine: puppeteer, playwright',
|
|
48
|
+
default: getenv('BROWSER_ENGINE', 'puppeteer'),
|
|
49
|
+
})
|
|
50
|
+
.option('configuration', {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'Path to .lenv configuration file',
|
|
53
|
+
})
|
|
54
|
+
.help('help')
|
|
55
|
+
.alias('help', 'h')
|
|
56
|
+
.version()
|
|
57
|
+
.alias('version', 'v')
|
|
58
|
+
.example('web-capture --serve', 'Start API server on port 3000')
|
|
59
|
+
.example(
|
|
60
|
+
'web-capture --serve --port 8080',
|
|
61
|
+
'Start API server on custom port'
|
|
62
|
+
)
|
|
63
|
+
.example(
|
|
64
|
+
'web-capture https://example.com',
|
|
65
|
+
'Capture URL as HTML to stdout'
|
|
66
|
+
)
|
|
67
|
+
.example(
|
|
68
|
+
'web-capture https://example.com --format markdown --output page.md',
|
|
69
|
+
'Capture URL as Markdown to file'
|
|
70
|
+
)
|
|
71
|
+
.example(
|
|
72
|
+
'web-capture https://example.com --format png --engine playwright -o screenshot.png',
|
|
73
|
+
'Capture screenshot using Playwright'
|
|
74
|
+
)
|
|
75
|
+
.epilogue(
|
|
76
|
+
'API Endpoints (in server mode):\n GET /html?url=<URL>&engine=<ENGINE> Get rendered HTML\n GET /markdown?url=<URL> Get Markdown conversion\n GET /image?url=<URL>&engine=<ENGINE> Get PNG screenshot\n GET /fetch?url=<URL> Proxy fetch\n GET /stream?url=<URL> Streaming proxy'
|
|
77
|
+
)
|
|
78
|
+
.strict(),
|
|
79
|
+
lenv: {
|
|
80
|
+
enabled: true,
|
|
81
|
+
path: '.lenv',
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
async function startServer(port) {
|
|
86
|
+
// Import the Express app
|
|
87
|
+
const { app } = await import('../src/index.js');
|
|
88
|
+
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const server = app.listen(port, () => {
|
|
91
|
+
console.log(`web-capture server listening on http://localhost:${port}`);
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log('Available endpoints:');
|
|
94
|
+
console.log(` GET /html?url=<URL> - Render page as HTML`);
|
|
95
|
+
console.log(` GET /markdown?url=<URL> - Convert page to Markdown`);
|
|
96
|
+
console.log(` GET /image?url=<URL> - Screenshot page as PNG`);
|
|
97
|
+
console.log(` GET /fetch?url=<URL> - Proxy fetch content`);
|
|
98
|
+
console.log(` GET /stream?url=<URL> - Stream content`);
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log('Press Ctrl+C to stop the server');
|
|
101
|
+
resolve(server);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
server.on('error', reject);
|
|
105
|
+
|
|
106
|
+
// Handle graceful shutdown
|
|
107
|
+
function shutdown(signal) {
|
|
108
|
+
console.log(`\nReceived ${signal}, shutting down...`);
|
|
109
|
+
server.close(() => {
|
|
110
|
+
console.log('Server closed');
|
|
111
|
+
process.exit(0);
|
|
112
|
+
});
|
|
113
|
+
// Force exit if not closed in 2 seconds
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
console.error('Force exiting after 2s');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}, 2000);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
121
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function captureUrl(url, options) {
|
|
126
|
+
const { format, output, engine } = options;
|
|
127
|
+
|
|
128
|
+
// Ensure URL is absolute
|
|
129
|
+
let absoluteUrl = url;
|
|
130
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
131
|
+
absoluteUrl = `https://${url}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Validate URL
|
|
135
|
+
try {
|
|
136
|
+
new URL(absoluteUrl);
|
|
137
|
+
} catch {
|
|
138
|
+
console.error(`Error: Invalid URL "${url}"`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Import required modules
|
|
143
|
+
const {
|
|
144
|
+
fetchHtml,
|
|
145
|
+
convertHtmlToMarkdown,
|
|
146
|
+
convertToUtf8,
|
|
147
|
+
convertRelativeUrls,
|
|
148
|
+
} = await import('../src/lib.js');
|
|
149
|
+
const { createBrowser } = await import('../src/browser.js');
|
|
150
|
+
|
|
151
|
+
const normalizedFormat = format.toLowerCase();
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
if (normalizedFormat === 'markdown' || normalizedFormat === 'md') {
|
|
155
|
+
// Markdown format
|
|
156
|
+
const html = await fetchHtml(absoluteUrl);
|
|
157
|
+
const markdown = convertHtmlToMarkdown(html, absoluteUrl);
|
|
158
|
+
|
|
159
|
+
if (output) {
|
|
160
|
+
fs.writeFileSync(output, markdown, 'utf-8');
|
|
161
|
+
console.error(`Markdown saved to: ${output}`);
|
|
162
|
+
} else {
|
|
163
|
+
process.stdout.write(markdown);
|
|
164
|
+
}
|
|
165
|
+
} else if (
|
|
166
|
+
normalizedFormat === 'image' ||
|
|
167
|
+
normalizedFormat === 'png' ||
|
|
168
|
+
normalizedFormat === 'screenshot'
|
|
169
|
+
) {
|
|
170
|
+
// Image/screenshot format
|
|
171
|
+
const browser = await createBrowser(engine);
|
|
172
|
+
try {
|
|
173
|
+
const page = await browser.newPage();
|
|
174
|
+
await page.setExtraHTTPHeaders({
|
|
175
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
176
|
+
'Accept-Charset': 'utf-8',
|
|
177
|
+
});
|
|
178
|
+
await page.setUserAgent(
|
|
179
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
180
|
+
);
|
|
181
|
+
await page.setViewport({ width: 1280, height: 800 });
|
|
182
|
+
await page.goto(absoluteUrl, {
|
|
183
|
+
waitUntil: 'networkidle0',
|
|
184
|
+
timeout: 30000,
|
|
185
|
+
});
|
|
186
|
+
// Wait for 5 seconds after page load
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
188
|
+
|
|
189
|
+
const buffer = await page.screenshot({ type: 'png' });
|
|
190
|
+
|
|
191
|
+
if (output) {
|
|
192
|
+
fs.writeFileSync(output, buffer);
|
|
193
|
+
console.error(`Screenshot saved to: ${output}`);
|
|
194
|
+
} else {
|
|
195
|
+
// Generate default filename based on URL
|
|
196
|
+
const urlObj = new URL(absoluteUrl);
|
|
197
|
+
const defaultFilename = `${urlObj.hostname.replace(/\./g, '_')}_${Date.now()}.png`;
|
|
198
|
+
fs.writeFileSync(defaultFilename, buffer);
|
|
199
|
+
console.error(`Screenshot saved to: ${defaultFilename}`);
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
await browser.close();
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
// HTML format (default)
|
|
206
|
+
const html = await fetchHtml(absoluteUrl);
|
|
207
|
+
const hasJavaScript =
|
|
208
|
+
/<script[^>]*>[\s\S]*?<\/script>|<script[^>]*\/>|javascript:/i.test(
|
|
209
|
+
html
|
|
210
|
+
);
|
|
211
|
+
const isHtml = /<html[^>]*>[\s\S]*?<\/html>/i.test(html);
|
|
212
|
+
|
|
213
|
+
let resultHtml;
|
|
214
|
+
|
|
215
|
+
if (!isHtml || hasJavaScript) {
|
|
216
|
+
// Use browser to get rendered HTML
|
|
217
|
+
const browser = await createBrowser(engine);
|
|
218
|
+
try {
|
|
219
|
+
const page = await browser.newPage();
|
|
220
|
+
await page.setExtraHTTPHeaders({
|
|
221
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
222
|
+
'Accept-Charset': 'utf-8',
|
|
223
|
+
});
|
|
224
|
+
await page.setUserAgent(
|
|
225
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
226
|
+
);
|
|
227
|
+
await page.setViewport({ width: 1280, height: 800 });
|
|
228
|
+
await page.goto(absoluteUrl, {
|
|
229
|
+
waitUntil: 'networkidle0',
|
|
230
|
+
timeout: 30000,
|
|
231
|
+
});
|
|
232
|
+
// Wait for 5 seconds after page load
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
234
|
+
|
|
235
|
+
const renderedHtml = await page.content();
|
|
236
|
+
const utf8Html = convertToUtf8(renderedHtml);
|
|
237
|
+
resultHtml = convertRelativeUrls(utf8Html, absoluteUrl);
|
|
238
|
+
} finally {
|
|
239
|
+
await browser.close();
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
// Plain HTML without JavaScript
|
|
243
|
+
const utf8Html = convertToUtf8(html);
|
|
244
|
+
resultHtml = convertRelativeUrls(utf8Html, absoluteUrl);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (output) {
|
|
248
|
+
fs.writeFileSync(output, resultHtml, 'utf-8');
|
|
249
|
+
console.error(`HTML saved to: ${output}`);
|
|
250
|
+
} else {
|
|
251
|
+
process.stdout.write(resultHtml);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error(`Error capturing ${absoluteUrl}: ${err.message}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function main() {
|
|
261
|
+
// Get positional arguments (non-option arguments)
|
|
262
|
+
const url = config._ && config._.length > 0 ? config._[0] : null;
|
|
263
|
+
|
|
264
|
+
if (config.serve) {
|
|
265
|
+
// Server mode
|
|
266
|
+
await startServer(config.port);
|
|
267
|
+
} else if (url) {
|
|
268
|
+
// Capture mode
|
|
269
|
+
await captureUrl(url, {
|
|
270
|
+
format: config.format,
|
|
271
|
+
output: config.output,
|
|
272
|
+
engine: config.engine,
|
|
273
|
+
});
|
|
274
|
+
} else {
|
|
275
|
+
// No arguments - show error
|
|
276
|
+
console.error('Error: Missing URL or --serve flag');
|
|
277
|
+
console.error('Run with --help for usage information');
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
main().catch((err) => {
|
|
283
|
+
console.error('Fatal error:', err.message);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Market Analysis Report: 13 May 2025
|
|
2
|
+
|
|
3
|
+
This report analyzes open-source projects similar to "web-capture," a microservice for capturing web content in HTML, Markdown, and PNG formats. The projects are grouped into **microservices** and **libraries**, with each evaluated for its features and relevance to web content capture. Below are two tables summarizing the findings, followed by detailed feature lists for each project.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Microservices for Web Capture
|
|
8
|
+
|
|
9
|
+
| **Project** | **License** | **GitHub Link** |
|
|
10
|
+
| ----------------------- | ----------- | ----------------------------------------------------------------------------------------- |
|
|
11
|
+
| **screeenly** | MIT | [stefanzweifel/screeenly](https://github.com/stefanzweifel/screeenly) |
|
|
12
|
+
| **capture-website-api** | ISC | [robvanderleek/capture-website-api](https://github.com/robvanderleek/capture-website-api) |
|
|
13
|
+
| **captino** | MIT | [pistatium/captino](https://github.com/pistatium/captino) |
|
|
14
|
+
| **gowitness** | MIT | [gowitness/gowitness](https://github.com/gowitness/gowitness) |
|
|
15
|
+
|
|
16
|
+
### Features of Microservices
|
|
17
|
+
|
|
18
|
+
- **screeenly**
|
|
19
|
+
- API for screenshots (PNG), PDFs, and HTML rendering
|
|
20
|
+
- Customizable via pixel density and image loading options
|
|
21
|
+
- Self-hosting support via Heroku, Zeet, or Docker
|
|
22
|
+
|
|
23
|
+
- **capture-website-api**
|
|
24
|
+
- API for website screenshots (PNG)
|
|
25
|
+
- Customizable via query parameters (e.g., width, height, scale)
|
|
26
|
+
- Uses Puppeteer for rendering complex sites
|
|
27
|
+
- Configurable via environment variables
|
|
28
|
+
|
|
29
|
+
- **captino**
|
|
30
|
+
- API for webpage screenshots (PNG)
|
|
31
|
+
- Supports full-page captures
|
|
32
|
+
- Easy deployment with Docker
|
|
33
|
+
|
|
34
|
+
- **gowitness**
|
|
35
|
+
- Command-line tool for website screenshots (PNG)
|
|
36
|
+
- Batch processing and report generation
|
|
37
|
+
- Built with Golang and Chrome Headless
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Libraries for Web Capture
|
|
42
|
+
|
|
43
|
+
| **Project** | **License** | **GitHub Link** |
|
|
44
|
+
| -------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| **webpage-capture** | MIT | [b4dnewz/webpage-capture](https://github.com/b4dnewz/webpage-capture) |
|
|
46
|
+
| **html-screen-capture-js** | MIT | [html-screen-capture-js/html-screen-capture-js](https://github.com/html-screen-capture-js/html-screen-capture-js) |
|
|
47
|
+
|
|
48
|
+
### Features of Libraries
|
|
49
|
+
|
|
50
|
+
- **webpage-capture**
|
|
51
|
+
- Captures web pages in PNG, JPEG, PDF, HTML, and other formats
|
|
52
|
+
- Customizable via options like timeout and headers
|
|
53
|
+
- Supports single and batch captures
|
|
54
|
+
|
|
55
|
+
- **html-screen-capture-js**
|
|
56
|
+
- Captures webpages as self-contained HTML
|
|
57
|
+
- Removes external dependencies while preserving appearance
|
|
58
|
+
- Lightweight (12KB)
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Summary
|
|
63
|
+
|
|
64
|
+
The report identifies four microservices (**screeenly**, **capture-website-api**, **captino**, **gowitness**) and two libraries (**webpage-capture**, **html-screen-capture-js**) that align with the functionality of "web-capture." Microservices provide API-driven solutions, while libraries offer flexibility for custom implementations. All projects are open-source, licensed under permissive terms (MIT or ISC), and actively maintained as of May 13, 2025.
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import prettierConfig from 'eslint-config-prettier';
|
|
3
|
+
import prettierPlugin from 'eslint-plugin-prettier';
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
js.configs.recommended,
|
|
7
|
+
prettierConfig,
|
|
8
|
+
{
|
|
9
|
+
files: ['**/*.js', '**/*.mjs'],
|
|
10
|
+
plugins: {
|
|
11
|
+
prettier: prettierPlugin,
|
|
12
|
+
},
|
|
13
|
+
languageOptions: {
|
|
14
|
+
ecmaVersion: 'latest',
|
|
15
|
+
sourceType: 'module',
|
|
16
|
+
globals: {
|
|
17
|
+
// Node.js globals
|
|
18
|
+
console: 'readonly',
|
|
19
|
+
process: 'readonly',
|
|
20
|
+
Buffer: 'readonly',
|
|
21
|
+
__dirname: 'readonly',
|
|
22
|
+
__filename: 'readonly',
|
|
23
|
+
global: 'readonly',
|
|
24
|
+
setTimeout: 'readonly',
|
|
25
|
+
clearTimeout: 'readonly',
|
|
26
|
+
setInterval: 'readonly',
|
|
27
|
+
clearInterval: 'readonly',
|
|
28
|
+
// Node.js 18+ globals
|
|
29
|
+
fetch: 'readonly',
|
|
30
|
+
// Runtime-specific globals
|
|
31
|
+
Bun: 'readonly',
|
|
32
|
+
Deno: 'readonly',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
rules: {
|
|
36
|
+
// Prettier integration
|
|
37
|
+
'prettier/prettier': 'error',
|
|
38
|
+
|
|
39
|
+
// Code quality rules
|
|
40
|
+
'no-unused-vars': 'error',
|
|
41
|
+
'no-console': 'off', // Allow console in this project
|
|
42
|
+
'no-debugger': 'error',
|
|
43
|
+
|
|
44
|
+
// Best practices
|
|
45
|
+
eqeqeq: ['error', 'always'],
|
|
46
|
+
curly: ['error', 'all'],
|
|
47
|
+
'no-var': 'error',
|
|
48
|
+
'prefer-const': 'error',
|
|
49
|
+
'prefer-arrow-callback': 'error',
|
|
50
|
+
'no-duplicate-imports': 'error',
|
|
51
|
+
|
|
52
|
+
// ES6+ features
|
|
53
|
+
'arrow-body-style': ['error', 'as-needed'],
|
|
54
|
+
'object-shorthand': ['error', 'always'],
|
|
55
|
+
'prefer-template': 'error',
|
|
56
|
+
|
|
57
|
+
// Async/await
|
|
58
|
+
'no-async-promise-executor': 'error',
|
|
59
|
+
'require-await': 'warn',
|
|
60
|
+
|
|
61
|
+
// Comments and documentation
|
|
62
|
+
'spaced-comment': ['error', 'always', { markers: ['/'] }],
|
|
63
|
+
|
|
64
|
+
// Complexity rules - reasonable thresholds for maintainability
|
|
65
|
+
complexity: ['warn', 15], // Cyclomatic complexity - allow more complex logic than strict 8
|
|
66
|
+
'max-depth': ['warn', 5], // Maximum nesting depth - slightly more lenient than strict 4
|
|
67
|
+
'max-lines-per-function': [
|
|
68
|
+
'warn',
|
|
69
|
+
{
|
|
70
|
+
max: 150, // More reasonable than strict 50 lines per function
|
|
71
|
+
skipBlankLines: true,
|
|
72
|
+
skipComments: true,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
'max-params': ['warn', 6], // Maximum function parameters - slightly more lenient than strict 5
|
|
76
|
+
'max-statements': ['warn', 60], // Maximum statements per function - reasonable limit for orchestration functions
|
|
77
|
+
'max-lines': ['error', 1500], // Maximum lines per file - counts all lines including blank lines and comments
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
// Test files have different requirements
|
|
82
|
+
files: ['tests/**/*.js', '**/*.test.js', '**/*.test.mjs'],
|
|
83
|
+
languageOptions: {
|
|
84
|
+
globals: {
|
|
85
|
+
// Jest globals
|
|
86
|
+
describe: 'readonly',
|
|
87
|
+
it: 'readonly',
|
|
88
|
+
test: 'readonly',
|
|
89
|
+
expect: 'readonly',
|
|
90
|
+
beforeAll: 'readonly',
|
|
91
|
+
afterAll: 'readonly',
|
|
92
|
+
beforeEach: 'readonly',
|
|
93
|
+
afterEach: 'readonly',
|
|
94
|
+
jest: 'readonly',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
rules: {
|
|
98
|
+
'require-await': 'off', // Async functions without await are common in tests
|
|
99
|
+
'max-lines-per-function': 'off', // Test files can have long test functions
|
|
100
|
+
'max-statements': 'off', // Test files can have many statements
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
ignores: [
|
|
105
|
+
'node_modules/**',
|
|
106
|
+
'coverage/**',
|
|
107
|
+
'dist/**',
|
|
108
|
+
'*.min.js',
|
|
109
|
+
'.eslintcache',
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
const baseUrl = 'http://localhost:3000';
|
|
5
|
+
const targetUrl = 'https://example.com';
|
|
6
|
+
|
|
7
|
+
console.log('Comparing Puppeteer and Playwright engines...\n');
|
|
8
|
+
|
|
9
|
+
// Fetch using Puppeteer
|
|
10
|
+
console.log('1. Fetching HTML using Puppeteer (default)...');
|
|
11
|
+
const startPuppeteer = Date.now();
|
|
12
|
+
const puppeteerResponse = await fetch(
|
|
13
|
+
`${baseUrl}/html?url=${encodeURIComponent(targetUrl)}`
|
|
14
|
+
);
|
|
15
|
+
const puppeteerHtml = await puppeteerResponse.text();
|
|
16
|
+
const puppeteerTime = Date.now() - startPuppeteer;
|
|
17
|
+
fs.writeFileSync('output/puppeteer_comparison.html', puppeteerHtml);
|
|
18
|
+
console.log(` Time: ${puppeteerTime}ms`);
|
|
19
|
+
console.log(` Size: ${puppeteerHtml.length} bytes`);
|
|
20
|
+
|
|
21
|
+
// Fetch using Playwright
|
|
22
|
+
console.log('\n2. Fetching HTML using Playwright...');
|
|
23
|
+
const startPlaywright = Date.now();
|
|
24
|
+
const playwrightResponse = await fetch(
|
|
25
|
+
`${baseUrl}/html?url=${encodeURIComponent(targetUrl)}&engine=playwright`
|
|
26
|
+
);
|
|
27
|
+
const playwrightHtml = await playwrightResponse.text();
|
|
28
|
+
const playwrightTime = Date.now() - startPlaywright;
|
|
29
|
+
fs.writeFileSync('output/playwright_comparison.html', playwrightHtml);
|
|
30
|
+
console.log(` Time: ${playwrightTime}ms`);
|
|
31
|
+
console.log(` Size: ${playwrightHtml.length} bytes`);
|
|
32
|
+
|
|
33
|
+
// Summary
|
|
34
|
+
console.log('\n=== Summary ===');
|
|
35
|
+
console.log(`Puppeteer: ${puppeteerTime}ms, ${puppeteerHtml.length} bytes`);
|
|
36
|
+
console.log(`Playwright: ${playwrightTime}ms, ${playwrightHtml.length} bytes`);
|
|
37
|
+
console.log(`Difference: ${Math.abs(puppeteerTime - playwrightTime)}ms`);
|
|
38
|
+
|
|
39
|
+
console.log('\nResults saved to output/ directory.');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const url = process.argv[2] || 'https://example.com';
|
|
7
|
+
const endpoint = `http://localhost:3000/html?url=${encodeURIComponent(url)}`;
|
|
8
|
+
|
|
9
|
+
fetch(endpoint)
|
|
10
|
+
.then((res) => {
|
|
11
|
+
if (!res.ok) {
|
|
12
|
+
throw new Error(`HTTP error! status: ${res.status}`);
|
|
13
|
+
}
|
|
14
|
+
return res.text();
|
|
15
|
+
})
|
|
16
|
+
.then((text) => {
|
|
17
|
+
const outputPath = path.join(__dirname, 'downloaded.html');
|
|
18
|
+
fs.writeFileSync(outputPath, text, 'utf-8');
|
|
19
|
+
console.log(`HTML saved to ${outputPath}`);
|
|
20
|
+
})
|
|
21
|
+
.catch((err) => {
|
|
22
|
+
console.error('Error:', err);
|
|
23
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const url = process.argv[2] || 'https://example.com';
|
|
7
|
+
const endpoint = `http://localhost:3000/image?url=${encodeURIComponent(url)}`;
|
|
8
|
+
|
|
9
|
+
fetch(endpoint)
|
|
10
|
+
.then((res) => {
|
|
11
|
+
console.log('Status:', res.status);
|
|
12
|
+
console.log('Content-Type:', res.headers.get('content-type'));
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
throw new Error(`HTTP error! status: ${res.status}`);
|
|
15
|
+
}
|
|
16
|
+
return res.arrayBuffer();
|
|
17
|
+
})
|
|
18
|
+
.then((buffer) => {
|
|
19
|
+
const buf = Buffer.from(buffer);
|
|
20
|
+
const pngSignature = Buffer.from([
|
|
21
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
22
|
+
]);
|
|
23
|
+
if (buf.slice(0, 8).equals(pngSignature)) {
|
|
24
|
+
const outputPath = path.join(__dirname, 'downloaded.png');
|
|
25
|
+
fs.writeFileSync(outputPath, buf);
|
|
26
|
+
console.log(`Image saved to ${outputPath} (valid PNG)`);
|
|
27
|
+
} else {
|
|
28
|
+
console.log(
|
|
29
|
+
'Response is not a valid PNG! First bytes:',
|
|
30
|
+
buf.slice(0, 16)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
.catch((err) => {
|
|
35
|
+
console.error('Error:', err);
|
|
36
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const url = process.argv[2] || 'https://example.com';
|
|
7
|
+
const endpoint = `http://localhost:3000/markdown?url=${encodeURIComponent(url)}`;
|
|
8
|
+
|
|
9
|
+
fetch(endpoint)
|
|
10
|
+
.then((res) => {
|
|
11
|
+
if (!res.ok) {
|
|
12
|
+
throw new Error(`HTTP error! status: ${res.status}`);
|
|
13
|
+
}
|
|
14
|
+
return res.text();
|
|
15
|
+
})
|
|
16
|
+
.then((text) => {
|
|
17
|
+
const outputPath = path.join(__dirname, 'downloaded.md');
|
|
18
|
+
fs.writeFileSync(outputPath, text, 'utf-8');
|
|
19
|
+
console.log(`Markdown saved to ${outputPath}`);
|
|
20
|
+
})
|
|
21
|
+
.catch((err) => {
|
|
22
|
+
console.error('Error:', err);
|
|
23
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
const baseUrl = 'http://localhost:3000';
|
|
5
|
+
const targetUrl = 'https://example.com';
|
|
6
|
+
|
|
7
|
+
console.log('Fetching HTML using Playwright engine...');
|
|
8
|
+
const htmlResponse = await fetch(
|
|
9
|
+
`${baseUrl}/html?url=${encodeURIComponent(targetUrl)}&engine=playwright`
|
|
10
|
+
);
|
|
11
|
+
const html = await htmlResponse.text();
|
|
12
|
+
fs.writeFileSync('output/playwright_html.html', html);
|
|
13
|
+
console.log('HTML saved to output/playwright_html.html');
|
|
14
|
+
|
|
15
|
+
console.log('Fetching screenshot using Playwright engine...');
|
|
16
|
+
const imageResponse = await fetch(
|
|
17
|
+
`${baseUrl}/image?url=${encodeURIComponent(targetUrl)}&engine=playwright`
|
|
18
|
+
);
|
|
19
|
+
const imageBuffer = await imageResponse.buffer();
|
|
20
|
+
fs.writeFileSync('output/playwright_screenshot.png', imageBuffer);
|
|
21
|
+
console.log('Screenshot saved to output/playwright_screenshot.png');
|
|
22
|
+
|
|
23
|
+
console.log('Done! Both requests used Playwright engine.');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
|
|
6
|
+
endpoint = f'http://localhost:3000/html?url={url}'
|
|
7
|
+
|
|
8
|
+
response = requests.get(endpoint)
|
|
9
|
+
response.raise_for_status()
|
|
10
|
+
|
|
11
|
+
output_path = os.path.join(os.path.dirname(__file__), 'downloaded.html')
|
|
12
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
13
|
+
f.write(response.text)
|
|
14
|
+
|
|
15
|
+
print(f'HTML saved to {output_path}')
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
|
|
6
|
+
endpoint = f'http://localhost:3000/image?url={url}'
|
|
7
|
+
|
|
8
|
+
response = requests.get(endpoint)
|
|
9
|
+
print('Status:', response.status_code)
|
|
10
|
+
print('Content-Type:', response.headers.get('content-type'))
|
|
11
|
+
|
|
12
|
+
# PNG signature: first 8 bytes
|
|
13
|
+
png_signature = b'\x89PNG\r\n\x1a\n'
|
|
14
|
+
if response.content[:8] == png_signature:
|
|
15
|
+
output_path = os.path.join(os.path.dirname(__file__), 'downloaded.png')
|
|
16
|
+
with open(output_path, 'wb') as f:
|
|
17
|
+
f.write(response.content)
|
|
18
|
+
print(f'Image saved to {output_path} (valid PNG)')
|
|
19
|
+
else:
|
|
20
|
+
print('Response is not a valid PNG! First bytes:', response.content[:16])
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
|
|
6
|
+
endpoint = f'http://localhost:3000/markdown?url={url}'
|
|
7
|
+
|
|
8
|
+
response = requests.get(endpoint)
|
|
9
|
+
response.raise_for_status()
|
|
10
|
+
|
|
11
|
+
output_path = os.path.join(os.path.dirname(__file__), 'downloaded.md')
|
|
12
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
13
|
+
f.write(response.text)
|
|
14
|
+
|
|
15
|
+
print(f'Markdown saved to {output_path}')
|