honkit 6.1.4 → 6.1.5

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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=watch.test.d.ts.map
@@ -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":"serve.d.ts","sourceRoot":"","sources":["../../src/cli/serve.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA4IA,wBA6DE"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/cli/serve.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAkJA,wBA6DE"}
package/lib/cli/serve.js CHANGED
@@ -62,51 +62,57 @@ function startServer(args, kwargs) {
62
62
  return waitForCtrlC();
63
63
  }
64
64
  // update book immutably. does not use book again
65
- return (0, watch_1.default)(book.getRoot(), (error, filepath) => {
66
- if (error) {
67
- console.error(error);
68
- return;
69
- }
70
- // If the file does not exist in file system, show a warning and skip
71
- // Probably, the file has been deleted
72
- if (!fs_1.default.existsSync(filepath)) {
73
- logger.warn.ok(`${filepath} does not exist in file system.`);
74
- return;
75
- }
76
- // set livereload path
77
- lrPath = filepath;
78
- // TODO: use parse extension
79
- // Incremental update for pages
80
- if (lastOutput && filepath.endsWith(".md")) {
81
- logger.warn.ok("Rebuild " + filepath);
82
- const changedOutput = lastOutput.reloadPage(lastOutput.book.getContentRoot(), filepath).merge({
83
- incrementalChangeFileSet: immutable_1.default.Set([filepath])
84
- });
85
- return incrementalBuild({
86
- output: changedOutput,
87
- Generator
88
- }).then(() => {
89
- if (lrPath && hasLiveReloading) {
90
- // trigger livereload
91
- lrServer.changed({
92
- body: {
93
- files: [lrPath]
94
- }
95
- });
96
- }
65
+ // Pass outputFolder to watch to prevent infinite rebuild loops
66
+ // https://github.com/honkit/honkit/issues/491
67
+ (0, watch_1.default)({
68
+ watchDir: book.getRoot(),
69
+ outputFolder,
70
+ callback: (error, filepath) => {
71
+ if (error) {
72
+ console.error(error);
73
+ return;
74
+ }
75
+ // If the file does not exist in file system, show a warning and skip
76
+ // Probably, the file has been deleted
77
+ if (!fs_1.default.existsSync(filepath)) {
78
+ logger.warn.ok(`${filepath} does not exist in file system.`);
79
+ return;
80
+ }
81
+ // set livereload path
82
+ lrPath = filepath;
83
+ // TODO: use parse extension
84
+ // Incremental update for pages
85
+ if (lastOutput && filepath.endsWith(".md")) {
86
+ logger.warn.ok("Rebuild " + filepath);
87
+ const changedOutput = lastOutput.reloadPage(lastOutput.book.getContentRoot(), filepath).merge({
88
+ incrementalChangeFileSet: immutable_1.default.Set([filepath])
89
+ });
90
+ return incrementalBuild({
91
+ output: changedOutput,
92
+ Generator
93
+ }).then(() => {
94
+ if (lrPath && hasLiveReloading) {
95
+ // trigger livereload
96
+ lrServer.changed({
97
+ body: {
98
+ files: [lrPath]
99
+ }
100
+ });
101
+ }
102
+ });
103
+ }
104
+ // Asciidoc files are not supported for incremental build
105
+ logger.info.ok("Rebuild " + filepath);
106
+ return generateBook({
107
+ book,
108
+ outputFolder,
109
+ hasLiveReloading,
110
+ Generator,
111
+ reload
112
+ }).then((output) => {
113
+ lastOutput = output;
97
114
  });
98
115
  }
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
116
  });
111
117
  });
112
118
  }
@@ -1,10 +1,26 @@
1
+ import { FSWatcher } from "chokidar";
2
+ export interface WatchOptions {
3
+ /**
4
+ * Directory to watch
5
+ */
6
+ watchDir: string;
7
+ /**
8
+ * Callback when a file is modified
9
+ */
10
+ callback: (error: Error | null, filepath?: string) => void;
11
+ /**
12
+ * Output folder to ignore (in addition to _book and node_modules)
13
+ * This prevents infinite rebuild loops when using custom output folders
14
+ * @see https://github.com/honkit/honkit/issues/491
15
+ */
16
+ outputFolder?: string;
17
+ }
1
18
  /**
2
- Watch a folder and resolve promise once a file is modified
3
-
4
- @param {string} dir
5
- @param callback
6
- @return {Promise}
19
+ * Watch a folder and call callback when a file is modified
20
+ *
21
+ * @param {WatchOptions} options
22
+ * @return {FSWatcher} The chokidar watcher instance
7
23
  */
8
- declare function watch(dir: any, callback: any): void;
24
+ declare function watch(options: WatchOptions): FSWatcher;
9
25
  export default watch;
10
26
  //# sourceMappingURL=watch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/cli/watch.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AAEH,iBAAS,KAAK,CAAC,GAAG,KAAA,EAAE,QAAQ,KAAA,QAwB3B;AAED,eAAe,KAAK,CAAC"}
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;;OAEG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D;;;;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,24 +7,37 @@ 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 resolve promise once a file is modified
11
-
12
- @param {string} dir
13
- @param callback
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(dir, callback) {
17
- dir = path_1.default.resolve(dir);
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
- // prevent infinity loop
26
- // https://github.com/honkit/honkit/issues/269
27
- ignored: ["_book/**", "node_modules/**"],
40
+ ignored: ignored,
28
41
  ignoreInitial: true
29
42
  });
30
43
  watcher.on("all", (e, filepath) => {
@@ -33,5 +46,6 @@ function watch(dir, callback) {
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.4",
3
+ "version": "6.1.5",
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/asciidoc": "6.1.4",
78
- "@honkit/honkit-plugin-fontsettings": "6.1.4",
79
- "@honkit/honkit-plugin-theme-default": "6.1.4",
80
- "@honkit/honkit-plugin-highlight": "6.1.4",
81
- "@honkit/html": "6.1.4",
82
- "@honkit/markdown-legacy": "6.1.4"
77
+ "@honkit/asciidoc": "6.1.5",
78
+ "@honkit/honkit-plugin-fontsettings": "6.1.5",
79
+ "@honkit/honkit-plugin-highlight": "6.1.5",
80
+ "@honkit/honkit-plugin-theme-default": "6.1.5",
81
+ "@honkit/markdown-legacy": "6.1.5",
82
+ "@honkit/html": "6.1.5"
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.4"
93
+ "@honkit/internal-test-utils": "6.1.5"
94
94
  },
95
95
  "scripts": {
96
96
  "build": "tsc -p .",