honkit 6.1.5 → 6.1.6
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/lib/cli/__tests__/build-integration.test.d.ts +2 -0
- package/lib/cli/__tests__/build-integration.test.d.ts.map +1 -0
- package/lib/cli/__tests__/build-integration.test.js +150 -0
- package/lib/cli/__tests__/getOutputFolder.test.d.ts +2 -0
- package/lib/cli/__tests__/getOutputFolder.test.d.ts.map +1 -0
- package/lib/cli/__tests__/getOutputFolder.test.js +38 -0
- package/lib/cli/__tests__/serve-integration.test.d.ts +2 -0
- package/lib/cli/__tests__/serve-integration.test.d.ts.map +1 -0
- package/lib/cli/__tests__/serve-integration.test.js +160 -0
- package/lib/cli/__tests__/shouldFullRebuild.test.d.ts +2 -0
- package/lib/cli/__tests__/shouldFullRebuild.test.d.ts.map +1 -0
- package/lib/cli/__tests__/shouldFullRebuild.test.js +78 -0
- package/lib/cli/getOutputFolder.d.ts +1 -1
- package/lib/cli/getOutputFolder.d.ts.map +1 -1
- package/lib/cli/getOutputFolder.js +8 -1
- package/lib/cli/serve.d.ts.map +1 -1
- package/lib/cli/serve.js +24 -14
- package/lib/cli/shouldFullRebuild.d.ts +26 -0
- package/lib/cli/shouldFullRebuild.d.ts.map +1 -0
- package/lib/cli/shouldFullRebuild.js +45 -0
- package/lib/cli/watch.d.ts +5 -1
- package/lib/cli/watch.d.ts.map +1 -1
- package/lib/cli/watch.js +2 -2
- package/package.json +8 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-integration.test.d.ts","sourceRoot":"","sources":["../../../src/cli/__tests__/build-integration.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const path_1 = __importDefault(require("path"));
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const bin_1 = require("../../bin");
|
|
10
|
+
const internal_test_utils_1 = require("@honkit/internal-test-utils");
|
|
11
|
+
/**
|
|
12
|
+
* Integration tests for honkit build output
|
|
13
|
+
*
|
|
14
|
+
* These tests use bin.run to execute the CLI and verify:
|
|
15
|
+
* 1. New files added to SUMMARY.md are correctly generated
|
|
16
|
+
* 2. Assets not in SUMMARY.md are copied correctly
|
|
17
|
+
* 3. Output structure matches expected snapshot
|
|
18
|
+
*
|
|
19
|
+
* This is a black-box test that only checks input (book files) and output (generated files).
|
|
20
|
+
*/
|
|
21
|
+
jest.setTimeout(60000);
|
|
22
|
+
describe("build integration", () => {
|
|
23
|
+
let tempDir;
|
|
24
|
+
let outputDir;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "honkit-build-test-"));
|
|
27
|
+
outputDir = path_1.default.join(tempDir, "_book");
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
if (tempDir && fs_1.default.existsSync(tempDir)) {
|
|
31
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
function createBookStructure() {
|
|
35
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "README.md"), `# Test Book
|
|
36
|
+
|
|
37
|
+
Welcome to the test book.
|
|
38
|
+
`);
|
|
39
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "SUMMARY.md"), `# Summary
|
|
40
|
+
|
|
41
|
+
* [Introduction](README.md)
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
async function buildBook() {
|
|
45
|
+
await (0, bin_1.run)([process.argv[0], "honkit", "build", tempDir, outputDir, "--reload"]);
|
|
46
|
+
}
|
|
47
|
+
function getOutputFiles() {
|
|
48
|
+
if (!fs_1.default.existsSync(outputDir))
|
|
49
|
+
return [];
|
|
50
|
+
const files = [];
|
|
51
|
+
const walk = (dir, prefix = "") => {
|
|
52
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
walk(path_1.default.join(dir, entry.name), relativePath);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
files.push(relativePath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
walk(outputDir);
|
|
64
|
+
return files.sort();
|
|
65
|
+
}
|
|
66
|
+
describe("build output with bin.run", () => {
|
|
67
|
+
it("should generate index.html for README.md", async () => {
|
|
68
|
+
createBookStructure();
|
|
69
|
+
await buildBook();
|
|
70
|
+
const files = getOutputFiles();
|
|
71
|
+
expect(files).toContain("index.html");
|
|
72
|
+
});
|
|
73
|
+
it("should generate chapter page when added to SUMMARY.md", async () => {
|
|
74
|
+
createBookStructure();
|
|
75
|
+
// Add chapter
|
|
76
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "chapter1.md"), `# Chapter 1
|
|
77
|
+
|
|
78
|
+
This is chapter 1.
|
|
79
|
+
`);
|
|
80
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "SUMMARY.md"), `# Summary
|
|
81
|
+
|
|
82
|
+
* [Introduction](README.md)
|
|
83
|
+
* [Chapter 1](chapter1.md)
|
|
84
|
+
`);
|
|
85
|
+
await buildBook();
|
|
86
|
+
const files = getOutputFiles();
|
|
87
|
+
expect(files).toContain("index.html");
|
|
88
|
+
expect(files).toContain("chapter1.html");
|
|
89
|
+
});
|
|
90
|
+
it("should copy asset file not in SUMMARY.md", async () => {
|
|
91
|
+
createBookStructure();
|
|
92
|
+
// Add asset file (not in SUMMARY)
|
|
93
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "notes.md"), `# Notes
|
|
94
|
+
|
|
95
|
+
These are my notes.
|
|
96
|
+
`);
|
|
97
|
+
await buildBook();
|
|
98
|
+
const files = getOutputFiles();
|
|
99
|
+
expect(files).toContain("index.html");
|
|
100
|
+
expect(files).toContain("notes.md"); // Copied as asset
|
|
101
|
+
});
|
|
102
|
+
it("should match expected output structure", async () => {
|
|
103
|
+
createBookStructure();
|
|
104
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "chapter1.md"), `# Chapter 1
|
|
105
|
+
|
|
106
|
+
Content of chapter 1.
|
|
107
|
+
`);
|
|
108
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "SUMMARY.md"), `# Summary
|
|
109
|
+
|
|
110
|
+
* [Introduction](README.md)
|
|
111
|
+
* [Chapter 1](chapter1.md)
|
|
112
|
+
`);
|
|
113
|
+
await buildBook();
|
|
114
|
+
const files = getOutputFiles();
|
|
115
|
+
// Filter out gitbook files and search index for cleaner snapshot
|
|
116
|
+
const relevantFiles = files.filter((f) => !f.startsWith("gitbook/") && !f.includes("search_index") && !f.includes("lunr"));
|
|
117
|
+
expect(relevantFiles).toMatchSnapshot("output-structure");
|
|
118
|
+
});
|
|
119
|
+
it("should generate correct HTML content", async () => {
|
|
120
|
+
createBookStructure();
|
|
121
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "chapter1.md"), `# Chapter 1
|
|
122
|
+
|
|
123
|
+
Content of chapter 1.
|
|
124
|
+
`);
|
|
125
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "SUMMARY.md"), `# Summary
|
|
126
|
+
|
|
127
|
+
* [Introduction](README.md)
|
|
128
|
+
* [Chapter 1](chapter1.md)
|
|
129
|
+
`);
|
|
130
|
+
await buildBook();
|
|
131
|
+
// Use iterateDirectoryContents like snapshot-honkit.ts
|
|
132
|
+
const maskContent = (content) => {
|
|
133
|
+
return content
|
|
134
|
+
.replace(/gitbook\.page\.hasChanged\(.*\);/g, "")
|
|
135
|
+
.replace(/<meta name="generator" content="HonKit .*">/g, "")
|
|
136
|
+
.replace(tempDir, "<TEMP_DIR>");
|
|
137
|
+
};
|
|
138
|
+
for await (const item of (0, internal_test_utils_1.iterateDirectoryContents)({
|
|
139
|
+
baseDir: outputDir,
|
|
140
|
+
allowExtensions: [".html"],
|
|
141
|
+
maskContent
|
|
142
|
+
})) {
|
|
143
|
+
// Only snapshot key pages, not all gitbook assets
|
|
144
|
+
if (item.filePath === "index.html" || item.filePath === "chapter1.html") {
|
|
145
|
+
expect(item).toMatchSnapshot(item.filePath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getOutputFolder.test.d.ts","sourceRoot":"","sources":["../../../src/cli/__tests__/getOutputFolder.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const path_1 = __importDefault(require("path"));
|
|
7
|
+
const getOutputFolder_1 = __importDefault(require("../getOutputFolder"));
|
|
8
|
+
describe("getOutputFolder", () => {
|
|
9
|
+
// Use platform-appropriate absolute paths for testing
|
|
10
|
+
const bookRoot = path_1.default.resolve("/book/root");
|
|
11
|
+
const absoluteOutput = path_1.default.resolve("/absolute/output");
|
|
12
|
+
describe("relative output path", () => {
|
|
13
|
+
it("should resolve relative to book root", () => {
|
|
14
|
+
const result = (0, getOutputFolder_1.default)([bookRoot, "./output"]);
|
|
15
|
+
expect(result).toBe(path_1.default.join(bookRoot, "output"));
|
|
16
|
+
});
|
|
17
|
+
it("should resolve relative path without ./ prefix", () => {
|
|
18
|
+
const result = (0, getOutputFolder_1.default)([bookRoot, "dist"]);
|
|
19
|
+
expect(result).toBe(path_1.default.join(bookRoot, "dist"));
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("absolute output path", () => {
|
|
23
|
+
it("should preserve absolute path as-is", () => {
|
|
24
|
+
const result = (0, getOutputFolder_1.default)([bookRoot, absoluteOutput]);
|
|
25
|
+
expect(result).toBe(absoluteOutput);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe("default output folder", () => {
|
|
29
|
+
it("should use _book in book root when no output specified", () => {
|
|
30
|
+
const result = (0, getOutputFolder_1.default)([bookRoot]);
|
|
31
|
+
expect(result).toBe(path_1.default.join(bookRoot, "_book"));
|
|
32
|
+
});
|
|
33
|
+
it("should use _book in cwd when no args", () => {
|
|
34
|
+
const result = (0, getOutputFolder_1.default)([]);
|
|
35
|
+
expect(result).toBe(path_1.default.join(process.cwd(), "_book"));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve-integration.test.d.ts","sourceRoot":"","sources":["../../../src/cli/__tests__/serve-integration.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const path_1 = __importDefault(require("path"));
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
/**
|
|
11
|
+
* Integration tests for honkit serve rebuild behavior
|
|
12
|
+
*
|
|
13
|
+
* These tests verify that:
|
|
14
|
+
* 1. SUMMARY.md changes trigger full rebuild and generate new pages
|
|
15
|
+
* 2. New file additions are detected and processed
|
|
16
|
+
*
|
|
17
|
+
* This is a black-box test that starts serve, modifies files, and checks output.
|
|
18
|
+
*/
|
|
19
|
+
jest.setTimeout(60000);
|
|
20
|
+
describe("serve integration", () => {
|
|
21
|
+
let tempDir;
|
|
22
|
+
let outputDir;
|
|
23
|
+
let serveProcess = null;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "honkit-serve-test-"));
|
|
26
|
+
outputDir = path_1.default.join(tempDir, "_book");
|
|
27
|
+
});
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
// Kill serve process
|
|
30
|
+
if (serveProcess && !serveProcess.killed) {
|
|
31
|
+
serveProcess.kill("SIGKILL");
|
|
32
|
+
await new Promise((resolve) => {
|
|
33
|
+
serveProcess?.on("exit", resolve);
|
|
34
|
+
setTimeout(resolve, 1000);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
serveProcess = null;
|
|
38
|
+
// Cleanup temp directory
|
|
39
|
+
if (tempDir && fs_1.default.existsSync(tempDir)) {
|
|
40
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
function createBookStructure() {
|
|
44
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "README.md"), `# Test Book
|
|
45
|
+
|
|
46
|
+
Welcome to the test book.
|
|
47
|
+
`);
|
|
48
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "SUMMARY.md"), `# Summary
|
|
49
|
+
|
|
50
|
+
* [Introduction](README.md)
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
function getOutputFiles() {
|
|
54
|
+
if (!fs_1.default.existsSync(outputDir))
|
|
55
|
+
return [];
|
|
56
|
+
const files = [];
|
|
57
|
+
const walk = (dir, prefix = "") => {
|
|
58
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
walk(path_1.default.join(dir, entry.name), relativePath);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
files.push(relativePath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
walk(outputDir);
|
|
70
|
+
return files.sort();
|
|
71
|
+
}
|
|
72
|
+
function startServe() {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const binPath = path_1.default.resolve(__dirname, "../../../bin/honkit.js");
|
|
75
|
+
// Use port 0 to let OS assign an available port
|
|
76
|
+
serveProcess = (0, child_process_1.spawn)(process.execPath, [binPath, "serve", tempDir, outputDir, "--port", "0", "--lrport", "0"], {
|
|
77
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
78
|
+
env: { ...process.env }
|
|
79
|
+
});
|
|
80
|
+
let started = false;
|
|
81
|
+
const checkStarted = (output) => {
|
|
82
|
+
if (output.includes("Serving book on") && !started) {
|
|
83
|
+
started = true;
|
|
84
|
+
// Wait a bit for initial build to complete
|
|
85
|
+
setTimeout(resolve, 1000);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
serveProcess.stdout?.on("data", (data) => {
|
|
89
|
+
checkStarted(data.toString());
|
|
90
|
+
});
|
|
91
|
+
serveProcess.stderr?.on("data", (data) => {
|
|
92
|
+
// HonKit outputs to stderr for info/warn
|
|
93
|
+
checkStarted(data.toString());
|
|
94
|
+
});
|
|
95
|
+
serveProcess.on("error", reject);
|
|
96
|
+
// Timeout after 10 seconds
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
if (!started) {
|
|
99
|
+
reject(new Error("Timeout waiting for serve to start"));
|
|
100
|
+
}
|
|
101
|
+
}, 10000);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function waitForFile(filePath, timeoutMs = 15000) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const startTime = Date.now();
|
|
107
|
+
const check = () => {
|
|
108
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
109
|
+
resolve();
|
|
110
|
+
}
|
|
111
|
+
else if (Date.now() - startTime > timeoutMs) {
|
|
112
|
+
reject(new Error(`Timeout waiting for file: ${filePath}`));
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
setTimeout(check, 200);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
check();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
describe("file watching and rebuild", () => {
|
|
122
|
+
it("should generate new page when SUMMARY.md is updated during serve", async () => {
|
|
123
|
+
createBookStructure();
|
|
124
|
+
await startServe();
|
|
125
|
+
// Verify initial build - only index.html
|
|
126
|
+
expect(getOutputFiles()).toContain("index.html");
|
|
127
|
+
expect(getOutputFiles()).not.toContain("chapter1.html");
|
|
128
|
+
// Add chapter1.md
|
|
129
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "chapter1.md"), `# Chapter 1
|
|
130
|
+
|
|
131
|
+
This is chapter 1.
|
|
132
|
+
`);
|
|
133
|
+
// Update SUMMARY.md to include chapter1
|
|
134
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "SUMMARY.md"), `# Summary
|
|
135
|
+
|
|
136
|
+
* [Introduction](README.md)
|
|
137
|
+
* [Chapter 1](chapter1.md)
|
|
138
|
+
`);
|
|
139
|
+
// Wait for rebuild
|
|
140
|
+
await waitForFile(path_1.default.join(outputDir, "chapter1.html"));
|
|
141
|
+
// Verify chapter1.html was generated
|
|
142
|
+
expect(getOutputFiles()).toContain("chapter1.html");
|
|
143
|
+
});
|
|
144
|
+
it("should copy new asset file when added during serve", async () => {
|
|
145
|
+
createBookStructure();
|
|
146
|
+
await startServe();
|
|
147
|
+
// Verify initial state
|
|
148
|
+
expect(getOutputFiles()).not.toContain("notes.md");
|
|
149
|
+
// Add new asset file (not in SUMMARY.md)
|
|
150
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, "notes.md"), `# Notes
|
|
151
|
+
|
|
152
|
+
These are my notes.
|
|
153
|
+
`);
|
|
154
|
+
// Wait for the asset to be copied
|
|
155
|
+
await waitForFile(path_1.default.join(outputDir, "notes.md"));
|
|
156
|
+
// Verify asset was copied
|
|
157
|
+
expect(getOutputFiles()).toContain("notes.md");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shouldFullRebuild.test.d.ts","sourceRoot":"","sources":["../../../src/cli/__tests__/shouldFullRebuild.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const shouldFullRebuild_1 = require("../shouldFullRebuild");
|
|
4
|
+
describe("shouldFullRebuild", () => {
|
|
5
|
+
describe("structure files (change event)", () => {
|
|
6
|
+
it("should return true for SUMMARY.md change", () => {
|
|
7
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/SUMMARY.md", "change")).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
it("should return true for SUMMARY.md (case insensitive)", () => {
|
|
10
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/summary.md", "change")).toBe(true);
|
|
11
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/Summary.md", "change")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it("should return true for GLOSSARY.md change", () => {
|
|
14
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/GLOSSARY.md", "change")).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
it("should return true for book.json change", () => {
|
|
17
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/book.json", "change")).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
it("should return true for book.js change", () => {
|
|
20
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/book.js", "change")).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe("new file additions (add event)", () => {
|
|
24
|
+
it("should return true for any new file", () => {
|
|
25
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/newfile.md", "add")).toBe(true);
|
|
26
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/image.png", "add")).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
it("should return true for new structure files", () => {
|
|
29
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/SUMMARY.md", "add")).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("file deletions (unlink event)", () => {
|
|
33
|
+
it("should return true for any deleted file", () => {
|
|
34
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/deleted.md", "unlink")).toBe(true);
|
|
35
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/image.png", "unlink")).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it("should return true for deleted structure files", () => {
|
|
38
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/SUMMARY.md", "unlink")).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe("content files (change event)", () => {
|
|
42
|
+
it("should return false for regular markdown file changes", () => {
|
|
43
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/README.md", "change")).toBe(false);
|
|
44
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/chapter1.md", "change")).toBe(false);
|
|
45
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/docs/page.md", "change")).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it("should return false for files with SUMMARY in path but different name", () => {
|
|
48
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/SUMMARY/page.md", "change")).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe("without eventType (backwards compatibility)", () => {
|
|
52
|
+
it("should return true for structure files", () => {
|
|
53
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/SUMMARY.md")).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it("should return false for regular files", () => {
|
|
56
|
+
expect((0, shouldFullRebuild_1.shouldFullRebuild)("/path/to/book/README.md")).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe("isStructureFile", () => {
|
|
61
|
+
it("should identify SUMMARY.md as structure file", () => {
|
|
62
|
+
expect((0, shouldFullRebuild_1.isStructureFile)("/path/to/SUMMARY.md")).toBe(true);
|
|
63
|
+
expect((0, shouldFullRebuild_1.isStructureFile)("/path/to/summary.md")).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it("should identify GLOSSARY.md as structure file", () => {
|
|
66
|
+
expect((0, shouldFullRebuild_1.isStructureFile)("/path/to/GLOSSARY.md")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it("should identify book.json as structure file", () => {
|
|
69
|
+
expect((0, shouldFullRebuild_1.isStructureFile)("/path/to/book.json")).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it("should identify book.js as structure file", () => {
|
|
72
|
+
expect((0, shouldFullRebuild_1.isStructureFile)("/path/to/book.js")).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
it("should NOT identify regular files as structure files", () => {
|
|
75
|
+
expect((0, shouldFullRebuild_1.isStructureFile)("/path/to/README.md")).toBe(false);
|
|
76
|
+
expect((0, shouldFullRebuild_1.isStructureFile)("/path/to/chapter.md")).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getOutputFolder.d.ts","sourceRoot":"","sources":["../../src/cli/getOutputFolder.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,iBAAS,eAAe,CAAC,IAAI,KAAA,
|
|
1
|
+
{"version":3,"file":"getOutputFolder.d.ts","sourceRoot":"","sources":["../../src/cli/getOutputFolder.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,iBAAS,eAAe,CAAC,IAAI,KAAA,OAa5B;AAED,eAAe,eAAe,CAAC"}
|
|
@@ -13,7 +13,14 @@ const path_1 = __importDefault(require("path"));
|
|
|
13
13
|
function getOutputFolder(args) {
|
|
14
14
|
const bookRoot = path_1.default.resolve(args[0] || process.cwd());
|
|
15
15
|
const defaultOutputRoot = path_1.default.join(bookRoot, "_book");
|
|
16
|
-
|
|
16
|
+
// Fix: Resolve relative output folder paths against the book root (not process.cwd()).
|
|
17
|
+
// Absolute paths are preserved as-is. This ensures the output folder is correctly ignored
|
|
18
|
+
// by the file watcher. See: https://github.com/honkit/honkit/issues/491
|
|
19
|
+
const outputFolder = args[1]
|
|
20
|
+
? path_1.default.isAbsolute(args[1])
|
|
21
|
+
? args[1]
|
|
22
|
+
: path_1.default.resolve(bookRoot, args[1])
|
|
23
|
+
: defaultOutputRoot;
|
|
17
24
|
return outputFolder;
|
|
18
25
|
}
|
|
19
26
|
exports.default = getOutputFolder;
|
package/lib/cli/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/cli/serve.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/cli/serve.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA8JA,wBA6DE"}
|
package/lib/cli/serve.js
CHANGED
|
@@ -15,9 +15,19 @@ const getBook_1 = __importDefault(require("./getBook"));
|
|
|
15
15
|
const getOutputFolder_1 = __importDefault(require("./getOutputFolder"));
|
|
16
16
|
const server_1 = __importDefault(require("./server"));
|
|
17
17
|
const watch_1 = __importDefault(require("./watch"));
|
|
18
|
+
const shouldFullRebuild_1 = require("./shouldFullRebuild");
|
|
18
19
|
const page_cache_1 = require("../output/page-cache");
|
|
19
20
|
const fs_1 = __importDefault(require("fs"));
|
|
20
21
|
let server, lrServer, lrPath;
|
|
22
|
+
function triggerLiveReload() {
|
|
23
|
+
if (lrPath) {
|
|
24
|
+
lrServer.changed({
|
|
25
|
+
body: {
|
|
26
|
+
files: [lrPath]
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
21
31
|
function waitForCtrlC() {
|
|
22
32
|
const d = promise_1.default.defer();
|
|
23
33
|
process.on("SIGINT", () => {
|
|
@@ -67,7 +77,7 @@ function startServer(args, kwargs) {
|
|
|
67
77
|
(0, watch_1.default)({
|
|
68
78
|
watchDir: book.getRoot(),
|
|
69
79
|
outputFolder,
|
|
70
|
-
callback: (error, filepath) => {
|
|
80
|
+
callback: (error, filepath, eventType) => {
|
|
71
81
|
if (error) {
|
|
72
82
|
console.error(error);
|
|
73
83
|
return;
|
|
@@ -80,9 +90,12 @@ function startServer(args, kwargs) {
|
|
|
80
90
|
}
|
|
81
91
|
// set livereload path
|
|
82
92
|
lrPath = filepath;
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
93
|
+
// Full rebuild is required for:
|
|
94
|
+
// 1. Structure files (SUMMARY.md, GLOSSARY.md, book.json, book.js)
|
|
95
|
+
// 2. New file additions (to update asset list and page structure)
|
|
96
|
+
const needsFullRebuild = (0, shouldFullRebuild_1.shouldFullRebuild)(filepath, eventType);
|
|
97
|
+
// Incremental update for existing pages (only for .md files that don't require full rebuild)
|
|
98
|
+
if (lastOutput && filepath.endsWith(".md") && !needsFullRebuild) {
|
|
86
99
|
logger.warn.ok("Rebuild " + filepath);
|
|
87
100
|
const changedOutput = lastOutput.reloadPage(lastOutput.book.getContentRoot(), filepath).merge({
|
|
88
101
|
incrementalChangeFileSet: immutable_1.default.Set([filepath])
|
|
@@ -91,18 +104,13 @@ function startServer(args, kwargs) {
|
|
|
91
104
|
output: changedOutput,
|
|
92
105
|
Generator
|
|
93
106
|
}).then(() => {
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
lrServer.changed({
|
|
97
|
-
body: {
|
|
98
|
-
files: [lrPath]
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
}
|
|
107
|
+
if (hasLiveReloading)
|
|
108
|
+
triggerLiveReload();
|
|
102
109
|
});
|
|
103
110
|
}
|
|
104
|
-
//
|
|
105
|
-
|
|
111
|
+
// Full rebuild for structure changes, new files, or non-markdown files
|
|
112
|
+
const reason = needsFullRebuild ? " (full rebuild)" : "";
|
|
113
|
+
logger.info.ok("Rebuild " + filepath + reason);
|
|
106
114
|
return generateBook({
|
|
107
115
|
book,
|
|
108
116
|
outputFolder,
|
|
@@ -111,6 +119,8 @@ function startServer(args, kwargs) {
|
|
|
111
119
|
reload
|
|
112
120
|
}).then((output) => {
|
|
113
121
|
lastOutput = output;
|
|
122
|
+
if (hasLiveReloading)
|
|
123
|
+
triggerLiveReload();
|
|
114
124
|
});
|
|
115
125
|
}
|
|
116
126
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { WatchEventType } from "./watch";
|
|
2
|
+
/**
|
|
3
|
+
* Determine if a file change requires a full rebuild based on the file path
|
|
4
|
+
*
|
|
5
|
+
* Full rebuild is needed when:
|
|
6
|
+
* - SUMMARY.md is changed (book structure changed)
|
|
7
|
+
* - GLOSSARY.md is changed (glossary definitions changed)
|
|
8
|
+
* - book.json or book.js is changed (configuration changed)
|
|
9
|
+
*
|
|
10
|
+
* @param filepath - The absolute path of the changed file
|
|
11
|
+
* @returns true if full rebuild is needed, false for incremental build
|
|
12
|
+
*/
|
|
13
|
+
export declare function isStructureFile(filepath: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Determine if a file change requires a full rebuild
|
|
16
|
+
*
|
|
17
|
+
* Full rebuild is needed when:
|
|
18
|
+
* - Structure files are changed (SUMMARY.md, GLOSSARY.md, book.json, book.js)
|
|
19
|
+
* - Files are added or removed (to update asset list and page structure)
|
|
20
|
+
*
|
|
21
|
+
* @param filepath - The absolute path of the changed file
|
|
22
|
+
* @param eventType - The type of file change event
|
|
23
|
+
* @returns true if full rebuild is needed, false for incremental build
|
|
24
|
+
*/
|
|
25
|
+
export declare function shouldFullRebuild(filepath: string, eventType?: WatchEventType): boolean;
|
|
26
|
+
//# sourceMappingURL=shouldFullRebuild.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shouldFullRebuild.d.ts","sourceRoot":"","sources":["../../src/cli/shouldFullRebuild.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,cAAc,GAAG,OAAO,CAQvF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isStructureFile = isStructureFile;
|
|
7
|
+
exports.shouldFullRebuild = shouldFullRebuild;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/**
|
|
10
|
+
* Determine if a file change requires a full rebuild based on the file path
|
|
11
|
+
*
|
|
12
|
+
* Full rebuild is needed when:
|
|
13
|
+
* - SUMMARY.md is changed (book structure changed)
|
|
14
|
+
* - GLOSSARY.md is changed (glossary definitions changed)
|
|
15
|
+
* - book.json or book.js is changed (configuration changed)
|
|
16
|
+
*
|
|
17
|
+
* @param filepath - The absolute path of the changed file
|
|
18
|
+
* @returns true if full rebuild is needed, false for incremental build
|
|
19
|
+
*/
|
|
20
|
+
function isStructureFile(filepath) {
|
|
21
|
+
const filename = path_1.default.basename(filepath);
|
|
22
|
+
const lowerFilename = filename.toLowerCase();
|
|
23
|
+
// Structure files that require full rebuild
|
|
24
|
+
const structureFiles = ["summary.md", "glossary.md", "book.json", "book.js"];
|
|
25
|
+
return structureFiles.includes(lowerFilename);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Determine if a file change requires a full rebuild
|
|
29
|
+
*
|
|
30
|
+
* Full rebuild is needed when:
|
|
31
|
+
* - Structure files are changed (SUMMARY.md, GLOSSARY.md, book.json, book.js)
|
|
32
|
+
* - Files are added or removed (to update asset list and page structure)
|
|
33
|
+
*
|
|
34
|
+
* @param filepath - The absolute path of the changed file
|
|
35
|
+
* @param eventType - The type of file change event
|
|
36
|
+
* @returns true if full rebuild is needed, false for incremental build
|
|
37
|
+
*/
|
|
38
|
+
function shouldFullRebuild(filepath, eventType) {
|
|
39
|
+
// File additions or deletions always require full rebuild
|
|
40
|
+
if (eventType === "add" || eventType === "unlink") {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
// Structure files require full rebuild
|
|
44
|
+
return isStructureFile(filepath);
|
|
45
|
+
}
|
package/lib/cli/watch.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FSWatcher } from "chokidar";
|
|
2
|
+
export type WatchEventType = "add" | "addDir" | "change" | "unlink" | "unlinkDir";
|
|
2
3
|
export interface WatchOptions {
|
|
3
4
|
/**
|
|
4
5
|
* Directory to watch
|
|
@@ -6,8 +7,11 @@ export interface WatchOptions {
|
|
|
6
7
|
watchDir: string;
|
|
7
8
|
/**
|
|
8
9
|
* Callback when a file is modified
|
|
10
|
+
* @param error - Error if any
|
|
11
|
+
* @param filepath - Absolute path of the changed file
|
|
12
|
+
* @param eventType - Type of change: "add", "change", "unlink", etc.
|
|
9
13
|
*/
|
|
10
|
-
callback: (error: Error | null, filepath?: string) => void;
|
|
14
|
+
callback: (error: Error | null, filepath?: string, eventType?: WatchEventType) => void;
|
|
11
15
|
/**
|
|
12
16
|
* Output folder to ignore (in addition to _book and node_modules)
|
|
13
17
|
* This prevents infinite rebuild loops when using custom output folders
|
package/lib/cli/watch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/cli/watch.ts"],"names":[],"mappings":"AACA,OAAiB,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG/C,MAAM,WAAW,YAAY;IACzB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB
|
|
1
|
+
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/cli/watch.ts"],"names":[],"mappings":"AACA,OAAiB,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG/C,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;AAElF,MAAM,WAAW,YAAY;IACzB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IACvF;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,iBAAS,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,SAAS,CA0C/C;AAED,eAAe,KAAK,CAAC"}
|
package/lib/cli/watch.js
CHANGED
|
@@ -40,8 +40,8 @@ function watch(options) {
|
|
|
40
40
|
ignored: ignored,
|
|
41
41
|
ignoreInitial: true
|
|
42
42
|
});
|
|
43
|
-
watcher.on("all", (
|
|
44
|
-
callback(null, path_1.default.resolve(dir, filepath));
|
|
43
|
+
watcher.on("all", (eventType, filepath) => {
|
|
44
|
+
callback(null, path_1.default.resolve(dir, filepath), eventType);
|
|
45
45
|
});
|
|
46
46
|
watcher.on("error", (err) => {
|
|
47
47
|
callback(err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "honkit",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.6",
|
|
4
4
|
"description": "HonKit is building beautiful books using Markdown.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -74,12 +74,12 @@
|
|
|
74
74
|
"tiny-lr": "^1.1.1",
|
|
75
75
|
"tmp": "0.2.4",
|
|
76
76
|
"urijs": "^1.19.11",
|
|
77
|
-
"@honkit/
|
|
78
|
-
"@honkit/honkit-plugin-fontsettings": "6.1.
|
|
79
|
-
"@honkit/
|
|
80
|
-
"@honkit/
|
|
81
|
-
"@honkit/
|
|
82
|
-
"@honkit/
|
|
77
|
+
"@honkit/honkit-plugin-highlight": "6.1.6",
|
|
78
|
+
"@honkit/honkit-plugin-fontsettings": "6.1.6",
|
|
79
|
+
"@honkit/html": "6.1.6",
|
|
80
|
+
"@honkit/markdown-legacy": "6.1.6",
|
|
81
|
+
"@honkit/asciidoc": "6.1.6",
|
|
82
|
+
"@honkit/honkit-plugin-theme-default": "6.1.6"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@relmify/jest-serializer-strip-ansi": "^1.0.2",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"jest-mock-process": "^2.0.0",
|
|
91
91
|
"rimraf": "^6.0.1",
|
|
92
92
|
"typescript": "^5.6.2",
|
|
93
|
-
"@honkit/internal-test-utils": "6.1.
|
|
93
|
+
"@honkit/internal-test-utils": "6.1.6"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|
|
96
96
|
"build": "tsc -p .",
|