mdv-live 0.5.8 → 0.5.9
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 +22 -0
- package/package.json +8 -7
- package/src/api/pdf.js +49 -17
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.9] - 2026-05-09
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **PDF export hang for plain Markdown** (regression since 2026-01-31 `e5526f9`):
|
|
13
|
+
- 原因 1: `md-to-pdf` が `package.json` に未宣言だったため `npx md-to-pdf` 経路に
|
|
14
|
+
依存。npx キャッシュ / TTY / レジストリ状況により挙動が不安定
|
|
15
|
+
- 原因 2: `child_process.execFile` は `stdio` オプションを受け付けない (Node 仕様)。
|
|
16
|
+
`md-to-pdf` 内部の `get-stdin` が EOF を待ち続け 180s SIGTERM していた
|
|
17
|
+
- 直し方: `md-to-pdf` を `dependencies` に追加 / `npx` 経由をやめて
|
|
18
|
+
`node_modules/.bin/md-to-pdf` を直接 spawn / `stdio: ['ignore', ...]` で stdin 即 EOF
|
|
19
|
+
- Marp 経路 (`marp-cli`) も同じ helper (`runPdfTool`) に統一して防御
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `tests/test-pdf-export.js`: PDF export 経路の自動テスト 5 件 (400 / 403 / 404 / plain
|
|
24
|
+
PDF / Marp PDF)。リグレッション再発防止
|
|
25
|
+
|
|
26
|
+
### Tests
|
|
27
|
+
|
|
28
|
+
- 236 → **241 件 (+5)**、全 PASS
|
|
29
|
+
|
|
8
30
|
## [0.5.8] - 2026-05-08
|
|
9
31
|
|
|
10
32
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdv-live",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "Markdown Viewer - File tree + Live preview + Marp support + Hot reload",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -45,16 +45,17 @@
|
|
|
45
45
|
"LICENSE"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"
|
|
49
|
-
"ws": "^8.18.0",
|
|
48
|
+
"@marp-team/marp-core": "^4.0.0",
|
|
50
49
|
"chokidar": "^4.0.3",
|
|
50
|
+
"express": "^4.21.2",
|
|
51
|
+
"highlight.js": "^11.10.0",
|
|
51
52
|
"markdown-it": "^14.1.0",
|
|
52
53
|
"markdown-it-task-lists": "^2.1.1",
|
|
53
|
-
"
|
|
54
|
-
"highlight.js": "^11.10.0",
|
|
55
|
-
"open": "^10.1.0",
|
|
54
|
+
"md-to-pdf": "^5.2.5",
|
|
56
55
|
"mime-types": "^2.1.35",
|
|
57
|
-
"multer": "^1.4.5-lts.1"
|
|
56
|
+
"multer": "^1.4.5-lts.1",
|
|
57
|
+
"open": "^10.1.0",
|
|
58
|
+
"ws": "^8.18.0"
|
|
58
59
|
},
|
|
59
60
|
"optionalDependencies": {
|
|
60
61
|
"@marp-team/marp-cli": "^4.0.3"
|
package/src/api/pdf.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from 'util';
|
|
1
|
+
import { spawn } from 'child_process';
|
|
3
2
|
import fs from 'fs/promises';
|
|
4
3
|
import path from 'path';
|
|
5
4
|
import { fileURLToPath } from 'url';
|
|
@@ -9,19 +8,55 @@ import { isMarp } from '../rendering/markdown.js';
|
|
|
9
8
|
import { validatePath, validatePathReal } from '../utils/path.js';
|
|
10
9
|
import { resolvePdfOptions } from '../styles/index.js';
|
|
11
10
|
|
|
12
|
-
const execFileAsync = promisify(execFile);
|
|
13
11
|
const require = createRequire(import.meta.url);
|
|
14
12
|
const highlightStylesheet = path.resolve(path.dirname(require.resolve('highlight.js')), '..', 'styles', 'atom-one-dark.css');
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'..',
|
|
19
|
-
'node_modules',
|
|
20
|
-
'.bin',
|
|
21
|
-
'marp'
|
|
22
|
-
);
|
|
13
|
+
const binDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'node_modules', '.bin');
|
|
14
|
+
const marpBin = path.join(binDir, 'marp');
|
|
15
|
+
const mdToPdfBin = path.join(binDir, 'md-to-pdf');
|
|
23
16
|
const PDF_EXPORT_TIMEOUT_MS = 180000;
|
|
24
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Spawn a PDF tool with stdin closed.
|
|
20
|
+
*
|
|
21
|
+
* 注意: execFile は stdio オプションを受け付けない (Node 仕様)。md-to-pdf は
|
|
22
|
+
* 内部で get-stdin を呼ぶため、stdin が pipe のままだと EOF を永遠に待ち続けて
|
|
23
|
+
* ハングする。spawn で stdin を 'ignore' (= /dev/null) に明示的に縛る。
|
|
24
|
+
*/
|
|
25
|
+
function runPdfTool(bin, args, { cwd } = {}) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const child = spawn(bin, args, {
|
|
28
|
+
cwd,
|
|
29
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
});
|
|
31
|
+
let stdout = '';
|
|
32
|
+
let stderr = '';
|
|
33
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
34
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
35
|
+
|
|
36
|
+
const timer = setTimeout(() => {
|
|
37
|
+
child.kill('SIGTERM');
|
|
38
|
+
}, PDF_EXPORT_TIMEOUT_MS);
|
|
39
|
+
|
|
40
|
+
child.on('error', (err) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
reject(err);
|
|
43
|
+
});
|
|
44
|
+
child.on('close', (code, signal) => {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
if (code === 0) {
|
|
47
|
+
resolve({ stdout, stderr });
|
|
48
|
+
} else {
|
|
49
|
+
const err = new Error(`${path.basename(bin)} exited with code=${code} signal=${signal}`);
|
|
50
|
+
err.code = code;
|
|
51
|
+
err.signal = signal;
|
|
52
|
+
err.stdout = stdout;
|
|
53
|
+
err.stderr = stderr;
|
|
54
|
+
reject(err);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
25
60
|
/**
|
|
26
61
|
* Resolve an optional user-selected file path under the server root.
|
|
27
62
|
* @param {string | undefined} relativePath - Path supplied by the web UI.
|
|
@@ -51,7 +86,7 @@ async function resolveOptionalUserFile(relativePath, rootDir) {
|
|
|
51
86
|
*/
|
|
52
87
|
async function exportMarkdownPdf(inputPath, outputPath, stylesheetPath, pdfOptionsPath) {
|
|
53
88
|
const pdfOptions = await resolvePdfOptions(pdfOptionsPath || undefined);
|
|
54
|
-
const args = [
|
|
89
|
+
const args = [inputPath, '--pdf-options', JSON.stringify(pdfOptions)];
|
|
55
90
|
|
|
56
91
|
if (stylesheetPath) {
|
|
57
92
|
args.push('--stylesheet', highlightStylesheet);
|
|
@@ -59,10 +94,7 @@ async function exportMarkdownPdf(inputPath, outputPath, stylesheetPath, pdfOptio
|
|
|
59
94
|
args.push('--highlight-style', 'atom-one-dark');
|
|
60
95
|
}
|
|
61
96
|
|
|
62
|
-
await
|
|
63
|
-
cwd: path.dirname(inputPath),
|
|
64
|
-
timeout: PDF_EXPORT_TIMEOUT_MS,
|
|
65
|
-
});
|
|
97
|
+
await runPdfTool(mdToPdfBin, args, { cwd: path.dirname(inputPath) });
|
|
66
98
|
|
|
67
99
|
const generatedPdf = inputPath.replace(/\.(md|markdown)$/i, '.pdf');
|
|
68
100
|
await fs.rename(generatedPdf, outputPath);
|
|
@@ -102,7 +134,7 @@ export function setupPdfRoutes(app) {
|
|
|
102
134
|
try {
|
|
103
135
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
104
136
|
if (isMarp(content)) {
|
|
105
|
-
await
|
|
137
|
+
await runPdfTool(marpBin, [fullPath, '-o', outputPath, '--html', '--allow-local-files', '--no-stdin']);
|
|
106
138
|
} else {
|
|
107
139
|
const [stylesheetPath, resolvedPdfOptionsPath] = await Promise.all([
|
|
108
140
|
resolveOptionalUserFile(stylePath, rootDir),
|