@link-assistant/web-capture 1.7.2 → 1.7.4
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/CHANGELOG.md +12 -0
- package/bin/web-capture.js +21 -2
- package/package.json +4 -4
- package/tests/unit/cli.test.js +88 -4
- package/tests/unit/package-runtime-dependencies.test.js +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/web-capture
|
|
2
2
|
|
|
3
|
+
## 1.7.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 494da4e: Move turndown-plugin-gfm into runtime dependencies so published CLI installs include the GFM plugin used by Markdown conversion.
|
|
8
|
+
|
|
9
|
+
## 1.7.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 237afeb: Fix the JavaScript CLI so strict argument parsing accepts positional capture URLs while still rejecting unknown options.
|
|
14
|
+
|
|
3
15
|
## 1.7.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/bin/web-capture.js
CHANGED
|
@@ -16,6 +16,12 @@ const config = makeConfig({
|
|
|
16
16
|
.usage(
|
|
17
17
|
'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'
|
|
18
18
|
)
|
|
19
|
+
.command('$0 [url]', 'Capture a URL or start the API server', (yargs) =>
|
|
20
|
+
yargs.positional('url', {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'URL to capture',
|
|
23
|
+
})
|
|
24
|
+
)
|
|
19
25
|
.option('serve', {
|
|
20
26
|
alias: 's',
|
|
21
27
|
type: 'boolean',
|
|
@@ -77,6 +83,7 @@ const config = makeConfig({
|
|
|
77
83
|
.option('output', {
|
|
78
84
|
alias: 'o',
|
|
79
85
|
type: 'string',
|
|
86
|
+
nargs: 1,
|
|
80
87
|
description:
|
|
81
88
|
'Output file path (default: stdout for text, auto-generated for images)',
|
|
82
89
|
})
|
|
@@ -232,6 +239,19 @@ const config = makeConfig({
|
|
|
232
239
|
},
|
|
233
240
|
});
|
|
234
241
|
|
|
242
|
+
function getUrlArgument() {
|
|
243
|
+
if (typeof config.url === 'string' && config.url.length > 0) {
|
|
244
|
+
return config.url;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const separatorIndex = process.argv.indexOf('--');
|
|
248
|
+
if (separatorIndex !== -1 && separatorIndex < process.argv.length - 1) {
|
|
249
|
+
return process.argv[separatorIndex + 1];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
235
255
|
async function startServer(port) {
|
|
236
256
|
// Import the Express app
|
|
237
257
|
const { app } = await import('../src/index.js');
|
|
@@ -824,8 +844,7 @@ async function captureUrl(url, options) {
|
|
|
824
844
|
}
|
|
825
845
|
|
|
826
846
|
async function main() {
|
|
827
|
-
|
|
828
|
-
const url = config._ && config._.length > 0 ? config._[0] : null;
|
|
847
|
+
const url = getUrlArgument();
|
|
829
848
|
|
|
830
849
|
if (config.serve) {
|
|
831
850
|
// Server mode
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/web-capture",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.4",
|
|
4
4
|
"description": "CLI and microservice to render web pages as HTML, Markdown, or PNG",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"playwright": "^1.49.0",
|
|
45
45
|
"puppeteer": "^24.8.2",
|
|
46
46
|
"puppeteer-core": "^24.40.0",
|
|
47
|
-
"turndown": "^7.1.1"
|
|
47
|
+
"turndown": "^7.1.1",
|
|
48
|
+
"turndown-plugin-gfm": "^1.0.2"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@babel/core": "^7.24.0",
|
|
@@ -62,8 +63,7 @@
|
|
|
62
63
|
"lint-staged": "^16.2.6",
|
|
63
64
|
"nock": "^13.5.4",
|
|
64
65
|
"prettier": "^3.6.2",
|
|
65
|
-
"supertest": "^6.3.4"
|
|
66
|
-
"turndown-plugin-gfm": "^1.0.2"
|
|
66
|
+
"supertest": "^6.3.4"
|
|
67
67
|
},
|
|
68
68
|
"type": "module",
|
|
69
69
|
"keywords": [
|
package/tests/unit/cli.test.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Unit tests for CLI argument parsing and functionality
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
|
+
import http from 'node:http';
|
|
3
4
|
import { fileURLToPath } from 'url';
|
|
4
5
|
import { dirname, resolve } from 'path';
|
|
5
6
|
|
|
@@ -33,6 +34,38 @@ function runCli(args, options = {}) {
|
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function startFixtureServer() {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const server = http.createServer((req, res) => {
|
|
40
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
41
|
+
res.end(
|
|
42
|
+
'<!doctype html><html><body><h1>Issue 68</h1><p>Captured from positional URL.</p></body></html>'
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
server.on('error', reject);
|
|
47
|
+
server.listen(0, '127.0.0.1', () => {
|
|
48
|
+
const { port } = server.address();
|
|
49
|
+
resolve({
|
|
50
|
+
server,
|
|
51
|
+
url: `http://127.0.0.1:${port}/article`,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stopFixtureServer(server) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
server.close((err) => {
|
|
60
|
+
if (err) {
|
|
61
|
+
reject(err);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
36
69
|
describe('CLI', () => {
|
|
37
70
|
describe('--help', () => {
|
|
38
71
|
test('shows help message with --help flag', async () => {
|
|
@@ -78,12 +111,63 @@ describe('CLI', () => {
|
|
|
78
111
|
});
|
|
79
112
|
|
|
80
113
|
describe('URL validation', () => {
|
|
114
|
+
test('accepts a URL as a positional argument', async () => {
|
|
115
|
+
const { server, url } = await startFixtureServer();
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const result = await runCli([
|
|
119
|
+
url,
|
|
120
|
+
'--format',
|
|
121
|
+
'markdown',
|
|
122
|
+
'--output',
|
|
123
|
+
'-',
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
expect(result.code).toBe(0);
|
|
127
|
+
expect(result.stderr).not.toContain('Unknown argument');
|
|
128
|
+
expect(result.stdout).toContain('Issue 68');
|
|
129
|
+
expect(result.stdout).toContain('Captured from positional URL.');
|
|
130
|
+
} finally {
|
|
131
|
+
await stopFixtureServer(server);
|
|
132
|
+
}
|
|
133
|
+
}, 15000);
|
|
134
|
+
|
|
135
|
+
test('accepts a URL after the -- argument separator', async () => {
|
|
136
|
+
const { server, url } = await startFixtureServer();
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const result = await runCli([
|
|
140
|
+
'--format',
|
|
141
|
+
'markdown',
|
|
142
|
+
'--output',
|
|
143
|
+
'-',
|
|
144
|
+
'--',
|
|
145
|
+
url,
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
expect(result.code).toBe(0);
|
|
149
|
+
expect(result.stderr).not.toContain('Missing URL');
|
|
150
|
+
expect(result.stdout).toContain('Issue 68');
|
|
151
|
+
} finally {
|
|
152
|
+
await stopFixtureServer(server);
|
|
153
|
+
}
|
|
154
|
+
}, 15000);
|
|
155
|
+
|
|
156
|
+
test('rejects unknown options', async () => {
|
|
157
|
+
const result = await runCli([
|
|
158
|
+
'--unknown-option-for-issue-68',
|
|
159
|
+
'https://example.com',
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
expect(result.code).toBe(1);
|
|
163
|
+
expect(result.stderr).toContain('Unknown argument');
|
|
164
|
+
});
|
|
165
|
+
|
|
81
166
|
test('rejects invalid URL', async () => {
|
|
82
|
-
const result = await runCli(['
|
|
83
|
-
// Should fail
|
|
167
|
+
const result = await runCli(['http://[invalid-url']);
|
|
168
|
+
// Should fail before trying to capture.
|
|
84
169
|
expect(result.code).toBe(1);
|
|
85
|
-
|
|
86
|
-
expect(result.stdout.length + result.stderr.length).toBeGreaterThan(0);
|
|
170
|
+
expect(result.stderr).toContain('Invalid URL');
|
|
87
171
|
}, 15000);
|
|
88
172
|
});
|
|
89
173
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { dirname, resolve } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
const jsRoot = resolve(__dirname, '../..');
|
|
8
|
+
|
|
9
|
+
function readJson(relativePath) {
|
|
10
|
+
return JSON.parse(readFileSync(resolve(jsRoot, relativePath), 'utf8'));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const runtimeImports = [
|
|
14
|
+
{
|
|
15
|
+
packageName: 'turndown-plugin-gfm',
|
|
16
|
+
sourceFile: 'src/lib.js',
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe('runtime package dependencies', () => {
|
|
21
|
+
test.each(runtimeImports)(
|
|
22
|
+
'$packageName is declared as a production dependency',
|
|
23
|
+
({ packageName, sourceFile }) => {
|
|
24
|
+
const source = readFileSync(resolve(jsRoot, sourceFile), 'utf8');
|
|
25
|
+
const packageJson = readJson('package.json');
|
|
26
|
+
|
|
27
|
+
expect(source).toContain(`from '${packageName}'`);
|
|
28
|
+
expect(packageJson.dependencies).toHaveProperty(packageName);
|
|
29
|
+
expect(packageJson.devDependencies || {}).not.toHaveProperty(packageName);
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
test.each(runtimeImports)(
|
|
34
|
+
'$packageName is not marked dev-only in package-lock.json',
|
|
35
|
+
({ packageName }) => {
|
|
36
|
+
const packageLock = readJson('package-lock.json');
|
|
37
|
+
|
|
38
|
+
expect(packageLock.packages[''].dependencies).toHaveProperty(packageName);
|
|
39
|
+
expect(packageLock.packages[''].devDependencies || {}).not.toHaveProperty(
|
|
40
|
+
packageName
|
|
41
|
+
);
|
|
42
|
+
expect(packageLock.packages[`node_modules/${packageName}`]).toBeDefined();
|
|
43
|
+
expect(packageLock.packages[`node_modules/${packageName}`].dev).not.toBe(
|
|
44
|
+
true
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
});
|