honkit 6.1.4 → 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/__tests__/watch.test.d.ts +2 -0
- package/lib/cli/__tests__/watch.test.d.ts.map +1 -0
- package/lib/cli/__tests__/watch.test.js +194 -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 +59 -43
- 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 +26 -6
- package/lib/cli/watch.d.ts.map +1 -1
- package/lib/cli/watch.js +26 -12
- 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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.test.d.ts","sourceRoot":"","sources":["../../../src/cli/__tests__/watch.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,194 @@
|
|
|
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 watch_1 = __importDefault(require("../watch"));
|
|
10
|
+
/**
|
|
11
|
+
* Normalize path separators to forward slashes for cross-platform comparison
|
|
12
|
+
*/
|
|
13
|
+
function normalizePath(filepath) {
|
|
14
|
+
return filepath.replace(/\\/g, "/");
|
|
15
|
+
}
|
|
16
|
+
describe("watch", () => {
|
|
17
|
+
// Increase timeout for file system operations (Windows may need more time)
|
|
18
|
+
jest.setTimeout(30000);
|
|
19
|
+
let tempDir;
|
|
20
|
+
let watchers = [];
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "honkit-watch-test-"));
|
|
23
|
+
});
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
// Close all watchers
|
|
26
|
+
await Promise.all(watchers.map((w) => w.close()));
|
|
27
|
+
watchers = [];
|
|
28
|
+
// Cleanup temp directory
|
|
29
|
+
if (tempDir && fs_1.default.existsSync(tempDir)) {
|
|
30
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
function createFile(relativePath, content = "# Test") {
|
|
34
|
+
const filePath = path_1.default.join(tempDir, relativePath);
|
|
35
|
+
const dir = path_1.default.dirname(filePath);
|
|
36
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
37
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
fs_1.default.writeFileSync(filePath, content);
|
|
40
|
+
return filePath;
|
|
41
|
+
}
|
|
42
|
+
function modifyFile(relativePath, content) {
|
|
43
|
+
const filePath = path_1.default.join(tempDir, relativePath);
|
|
44
|
+
fs_1.default.writeFileSync(filePath, content);
|
|
45
|
+
return filePath;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Wait for watcher to be ready and stable
|
|
49
|
+
*/
|
|
50
|
+
async function waitForReady(watcher) {
|
|
51
|
+
await new Promise((resolve) => {
|
|
52
|
+
watcher.on("ready", resolve);
|
|
53
|
+
});
|
|
54
|
+
// Give the watcher a moment to stabilize after ready event
|
|
55
|
+
// Windows needs more time for file system operations
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Wait for a file change to be detected that matches the predicate
|
|
60
|
+
* Paths are normalized to forward slashes for cross-platform compatibility
|
|
61
|
+
*/
|
|
62
|
+
function waitForChange(watcher, predicate, timeoutMs = 20000) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const timeout = setTimeout(() => {
|
|
65
|
+
reject(new Error(`Timeout waiting for file change after ${timeoutMs}ms`));
|
|
66
|
+
}, timeoutMs);
|
|
67
|
+
const handler = (_event, filepath) => {
|
|
68
|
+
const fullPath = normalizePath(path_1.default.resolve(tempDir, filepath));
|
|
69
|
+
if (predicate(fullPath)) {
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
watcher.off("all", handler);
|
|
72
|
+
resolve(fullPath);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
watcher.on("all", handler);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
describe("issue #491 - custom output folder handling", () => {
|
|
79
|
+
it("should detect changes in custom output folder when not specified in options", async () => {
|
|
80
|
+
createFile("README.md", "# README");
|
|
81
|
+
createFile("output/orphan.md", "# Orphan in output folder");
|
|
82
|
+
const detectedFiles = [];
|
|
83
|
+
// Start watching WITHOUT specifying output folder
|
|
84
|
+
const watcher = (0, watch_1.default)({
|
|
85
|
+
watchDir: tempDir,
|
|
86
|
+
callback: (error, filepath) => {
|
|
87
|
+
if (error || !filepath)
|
|
88
|
+
return;
|
|
89
|
+
detectedFiles.push(normalizePath(filepath));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
watchers.push(watcher);
|
|
93
|
+
await waitForReady(watcher);
|
|
94
|
+
// Modify file in output folder and wait for detection
|
|
95
|
+
modifyFile("output/orphan.md", "# Modified");
|
|
96
|
+
const detected = await waitForChange(watcher, (f) => f.includes("output/orphan.md"));
|
|
97
|
+
expect(detected).toContain("output/orphan.md");
|
|
98
|
+
});
|
|
99
|
+
it("should NOT detect changes in custom output folder when specified in options", async () => {
|
|
100
|
+
createFile("README.md", "# README");
|
|
101
|
+
createFile("output/orphan.md", "# Orphan in output folder");
|
|
102
|
+
const detectedFiles = [];
|
|
103
|
+
// Start watching WITH output folder specified - it should be ignored
|
|
104
|
+
const watcher = (0, watch_1.default)({
|
|
105
|
+
watchDir: tempDir,
|
|
106
|
+
outputFolder: "output",
|
|
107
|
+
callback: (error, filepath) => {
|
|
108
|
+
if (error || !filepath)
|
|
109
|
+
return;
|
|
110
|
+
detectedFiles.push(normalizePath(filepath));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
watchers.push(watcher);
|
|
114
|
+
await waitForReady(watcher);
|
|
115
|
+
// Modify both files - output should be ignored, README should be detected
|
|
116
|
+
modifyFile("output/orphan.md", "# Modified");
|
|
117
|
+
modifyFile("README.md", "# Modified README");
|
|
118
|
+
// Wait for README change to be detected (proves watcher is working)
|
|
119
|
+
await waitForChange(watcher, (f) => f.includes("README.md"));
|
|
120
|
+
// Verify output folder change was NOT detected
|
|
121
|
+
const outputChanges = detectedFiles.filter((f) => f.includes("output/"));
|
|
122
|
+
expect(outputChanges).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
it("should handle absolute output folder paths", async () => {
|
|
125
|
+
createFile("README.md", "# README");
|
|
126
|
+
createFile("docs-output/file.md", "# File");
|
|
127
|
+
const detectedFiles = [];
|
|
128
|
+
const absoluteOutputPath = path_1.default.join(tempDir, "docs-output");
|
|
129
|
+
const watcher = (0, watch_1.default)({
|
|
130
|
+
watchDir: tempDir,
|
|
131
|
+
outputFolder: absoluteOutputPath,
|
|
132
|
+
callback: (error, filepath) => {
|
|
133
|
+
if (error || !filepath)
|
|
134
|
+
return;
|
|
135
|
+
detectedFiles.push(normalizePath(filepath));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
watchers.push(watcher);
|
|
139
|
+
await waitForReady(watcher);
|
|
140
|
+
// Modify both files
|
|
141
|
+
modifyFile("docs-output/file.md", "# Modified");
|
|
142
|
+
modifyFile("README.md", "# Modified README");
|
|
143
|
+
// Wait for README change
|
|
144
|
+
await waitForChange(watcher, (f) => f.includes("README.md"));
|
|
145
|
+
// Verify docs-output was NOT detected
|
|
146
|
+
const outputChanges = detectedFiles.filter((f) => f.includes("docs-output/"));
|
|
147
|
+
expect(outputChanges).toHaveLength(0);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe("default _book folder", () => {
|
|
151
|
+
it("should NOT detect changes in _book folder", async () => {
|
|
152
|
+
createFile("file1.md", "# File 1");
|
|
153
|
+
createFile("file2.md", "# File 2");
|
|
154
|
+
createFile("_book/output.md", "# Output");
|
|
155
|
+
const detectedFiles = [];
|
|
156
|
+
const watcher = (0, watch_1.default)({
|
|
157
|
+
watchDir: tempDir,
|
|
158
|
+
callback: (error, filepath) => {
|
|
159
|
+
if (error || !filepath)
|
|
160
|
+
return;
|
|
161
|
+
detectedFiles.push(normalizePath(filepath));
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
watchers.push(watcher);
|
|
165
|
+
await waitForReady(watcher);
|
|
166
|
+
// First modify file1 to ensure watcher is working
|
|
167
|
+
modifyFile("file1.md", "# Modified File 1");
|
|
168
|
+
await waitForChange(watcher, (f) => f.includes("file1.md"));
|
|
169
|
+
// Now modify _book file (should be ignored)
|
|
170
|
+
modifyFile("_book/output.md", "# Modified");
|
|
171
|
+
// Modify file2 to confirm watcher still works
|
|
172
|
+
modifyFile("file2.md", "# Modified File 2");
|
|
173
|
+
await waitForChange(watcher, (f) => f.includes("file2.md"));
|
|
174
|
+
// Verify _book was NOT detected
|
|
175
|
+
const bookChanges = detectedFiles.filter((f) => f.includes("_book"));
|
|
176
|
+
expect(bookChanges).toHaveLength(0);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
describe("source files", () => {
|
|
180
|
+
it("should detect changes to markdown files in source directory", async () => {
|
|
181
|
+
createFile("README.md", "# README");
|
|
182
|
+
createFile("orphan.md", "# Orphan - not in SUMMARY");
|
|
183
|
+
const watcher = (0, watch_1.default)({
|
|
184
|
+
watchDir: tempDir,
|
|
185
|
+
callback: () => { }
|
|
186
|
+
});
|
|
187
|
+
watchers.push(watcher);
|
|
188
|
+
await waitForReady(watcher);
|
|
189
|
+
modifyFile("orphan.md", "# Modified orphan");
|
|
190
|
+
const detected = await waitForChange(watcher, (f) => f.includes("orphan.md"));
|
|
191
|
+
expect(detected).toContain("orphan.md");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -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", () => {
|
|
@@ -62,51 +72,57 @@ function startServer(args, kwargs) {
|
|
|
62
72
|
return waitForCtrlC();
|
|
63
73
|
}
|
|
64
74
|
// update book immutably. does not use book again
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
75
|
+
// Pass outputFolder to watch to prevent infinite rebuild loops
|
|
76
|
+
// https://github.com/honkit/honkit/issues/491
|
|
77
|
+
(0, watch_1.default)({
|
|
78
|
+
watchDir: book.getRoot(),
|
|
79
|
+
outputFolder,
|
|
80
|
+
callback: (error, filepath, eventType) => {
|
|
81
|
+
if (error) {
|
|
82
|
+
console.error(error);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// If the file does not exist in file system, show a warning and skip
|
|
86
|
+
// Probably, the file has been deleted
|
|
87
|
+
if (!fs_1.default.existsSync(filepath)) {
|
|
88
|
+
logger.warn.ok(`${filepath} does not exist in file system.`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// set livereload path
|
|
92
|
+
lrPath = filepath;
|
|
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) {
|
|
99
|
+
logger.warn.ok("Rebuild " + filepath);
|
|
100
|
+
const changedOutput = lastOutput.reloadPage(lastOutput.book.getContentRoot(), filepath).merge({
|
|
101
|
+
incrementalChangeFileSet: immutable_1.default.Set([filepath])
|
|
102
|
+
});
|
|
103
|
+
return incrementalBuild({
|
|
104
|
+
output: changedOutput,
|
|
105
|
+
Generator
|
|
106
|
+
}).then(() => {
|
|
107
|
+
if (hasLiveReloading)
|
|
108
|
+
triggerLiveReload();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
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);
|
|
114
|
+
return generateBook({
|
|
115
|
+
book,
|
|
116
|
+
outputFolder,
|
|
117
|
+
hasLiveReloading,
|
|
118
|
+
Generator,
|
|
119
|
+
reload
|
|
120
|
+
}).then((output) => {
|
|
121
|
+
lastOutput = output;
|
|
122
|
+
if (hasLiveReloading)
|
|
123
|
+
triggerLiveReload();
|
|
97
124
|
});
|
|
98
125
|
}
|
|
99
|
-
// Asciidoc files are not supported for incremental build
|
|
100
|
-
logger.info.ok("Rebuild " + filepath);
|
|
101
|
-
return generateBook({
|
|
102
|
-
book,
|
|
103
|
-
outputFolder,
|
|
104
|
-
hasLiveReloading,
|
|
105
|
-
Generator,
|
|
106
|
-
reload
|
|
107
|
-
}).then((output) => {
|
|
108
|
-
lastOutput = output;
|
|
109
|
-
});
|
|
110
126
|
});
|
|
111
127
|
});
|
|
112
128
|
}
|
|
@@ -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,10 +1,30 @@
|
|
|
1
|
+
import { FSWatcher } from "chokidar";
|
|
2
|
+
export type WatchEventType = "add" | "addDir" | "change" | "unlink" | "unlinkDir";
|
|
3
|
+
export interface WatchOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Directory to watch
|
|
6
|
+
*/
|
|
7
|
+
watchDir: string;
|
|
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.
|
|
13
|
+
*/
|
|
14
|
+
callback: (error: Error | null, filepath?: string, eventType?: WatchEventType) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Output folder to ignore (in addition to _book and node_modules)
|
|
17
|
+
* This prevents infinite rebuild loops when using custom output folders
|
|
18
|
+
* @see https://github.com/honkit/honkit/issues/491
|
|
19
|
+
*/
|
|
20
|
+
outputFolder?: string;
|
|
21
|
+
}
|
|
1
22
|
/**
|
|
2
|
-
Watch a folder and
|
|
3
|
-
|
|
4
|
-
@param {
|
|
5
|
-
@
|
|
6
|
-
@return {Promise}
|
|
23
|
+
* Watch a folder and call callback when a file is modified
|
|
24
|
+
*
|
|
25
|
+
* @param {WatchOptions} options
|
|
26
|
+
* @return {FSWatcher} The chokidar watcher instance
|
|
7
27
|
*/
|
|
8
|
-
declare function watch(
|
|
28
|
+
declare function watch(options: WatchOptions): FSWatcher;
|
|
9
29
|
export default watch;
|
|
10
30
|
//# sourceMappingURL=watch.d.ts.map
|
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":"
|
|
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
|
@@ -7,31 +7,45 @@ const path_1 = __importDefault(require("path"));
|
|
|
7
7
|
const chokidar_1 = __importDefault(require("chokidar"));
|
|
8
8
|
const parsers_1 = __importDefault(require("../parsers"));
|
|
9
9
|
/**
|
|
10
|
-
Watch a folder and
|
|
11
|
-
|
|
12
|
-
@param {
|
|
13
|
-
@
|
|
14
|
-
@return {Promise}
|
|
10
|
+
* Watch a folder and call callback when a file is modified
|
|
11
|
+
*
|
|
12
|
+
* @param {WatchOptions} options
|
|
13
|
+
* @return {FSWatcher} The chokidar watcher instance
|
|
15
14
|
*/
|
|
16
|
-
function watch(
|
|
17
|
-
|
|
15
|
+
function watch(options) {
|
|
16
|
+
const { callback, outputFolder } = options;
|
|
17
|
+
const dir = path_1.default.resolve(options.watchDir);
|
|
18
18
|
const toWatch = ["book.json", "book.js", "_layouts/**"];
|
|
19
19
|
// Watch all parsable files
|
|
20
20
|
parsers_1.default.extensions.forEach((ext) => {
|
|
21
21
|
toWatch.push(`**/*${ext}`);
|
|
22
22
|
});
|
|
23
|
+
// Build ignored patterns
|
|
24
|
+
// Always ignore _book and node_modules
|
|
25
|
+
// https://github.com/honkit/honkit/issues/269
|
|
26
|
+
const ignored = ["_book/**", "node_modules/**"];
|
|
27
|
+
// If a custom output folder is specified, ignore it too
|
|
28
|
+
// This prevents infinite rebuild loops when output folder is inside the watched directory
|
|
29
|
+
// https://github.com/honkit/honkit/issues/491
|
|
30
|
+
if (outputFolder) {
|
|
31
|
+
// Convert to forward slashes for glob pattern (Windows compatibility)
|
|
32
|
+
const outputRelative = path_1.default.relative(dir, path_1.default.resolve(dir, outputFolder)).replace(/\\/g, "/");
|
|
33
|
+
// Only add to ignored if the output folder is inside the watched directory
|
|
34
|
+
if (outputRelative && !outputRelative.startsWith("..") && !path_1.default.isAbsolute(outputRelative)) {
|
|
35
|
+
ignored.push(`${outputRelative}/**`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
23
38
|
const watcher = chokidar_1.default.watch(toWatch, {
|
|
24
39
|
cwd: dir,
|
|
25
|
-
|
|
26
|
-
// https://github.com/honkit/honkit/issues/269
|
|
27
|
-
ignored: ["_book/**", "node_modules/**"],
|
|
40
|
+
ignored: ignored,
|
|
28
41
|
ignoreInitial: true
|
|
29
42
|
});
|
|
30
|
-
watcher.on("all", (
|
|
31
|
-
callback(null, path_1.default.resolve(dir, filepath));
|
|
43
|
+
watcher.on("all", (eventType, filepath) => {
|
|
44
|
+
callback(null, path_1.default.resolve(dir, filepath), eventType);
|
|
32
45
|
});
|
|
33
46
|
watcher.on("error", (err) => {
|
|
34
47
|
callback(err);
|
|
35
48
|
});
|
|
49
|
+
return watcher;
|
|
36
50
|
}
|
|
37
51
|
exports.default = watch;
|
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 .",
|